Skip to content

Commit

Permalink
Add cache_disabled extension (#109)
Browse files Browse the repository at this point in the history
* Add cache_disabled extension

* Add test and implementation for httpcore

* Add extensions section

* revise changelog and document extensions

* Update CHANGELOG.md

Co-authored-by: Kar Petrosyan <[email protected]>

* update and simplify extension docs

---------

Co-authored-by: karpetrosyan <[email protected]>
Co-authored-by: Kar Petrosyan <[email protected]>
  • Loading branch information
3 people authored Nov 20, 2023
1 parent 03d8189 commit a6b4c65
Show file tree
Hide file tree
Showing 12 changed files with 290 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Unreleased

- Fix issue where freshness cannot be calculated to re-send request. (#104)
- Add `cache_disabled` extension to temporarily disable the cache (#109)
- Update `datetime.datetime.utcnow()` to `datetime.datetime.now(datetime.timezone.utc)` since `datetime.datetime.utcnow()` has been deprecated. (#111)

## 0.0.17 (6/11/2023)
Expand Down
76 changes: 76 additions & 0 deletions docs/advanced/extensions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
---
icon: material/apps
---

# Extensions

`HTTPX` provides an extension mechanism to allow additional information
to be added to requests and to be returned in responses. `hishel` makes use
of these extensions to expose some additional cache-related options and metadata.
These extensions are available from either the `hishel.CacheClient` /
`hishel.AsyncCacheClient` or a `httpx.Client` / `httpx.AsyncCacheClient`
using a `hishel` transport.

## Request extensions

### cache_disabled

This extension temporarily disables the cache by passing appropriate RFC9111 headers to
ignore cached responses and to not store incoming responses. For example:

```python
>>> import hishel
>>> client = hishel.CacheClient()
>>> response = client.get("https://www.example.com/cacheable-endpoint", extensions={"cache_disabled": True})

```
This feature is more fully documented in the [User Guide](/userguide/#temporarily-disabling-the-cache)

## Response extensions

### from_cache

Every response from will have a `from_cache` extension value that will be `True` when the response was retrieved
from the cache, and `False` when the response was received over the network.

```python
>>> import hishel
>>> client = hishel.CacheClient()
>>> response = client.get("https://www.example.com")
>>> response.extensions["from_cache"]
False
>>> response = client.get("https://www.example.com")
>>> response.extensions["from_cache"]
True
```

### cache_metadata

If `from_cache` is `True`, the response will also include a `cache_metadata` extension with additional information about
the response retrieved from the cache. If `from_cache` is `False`, then `cache_metadata` will not
be present in the response extensions.

Example:

```python
>>> import hishel
>>> client = hishel.CacheClient()
>>> response = client.get("https://www.example.com/cacheable-endpoint")
>>> response.extensions
{
... # other extensions
"from_cache": False
}
>>> response = client.get("https://www.example.com/cacheable-endpoint")
>>> response.extensions
{
... # other extensions
"from_cache": True
"cache_metadata" : {
"cache_key': '1a4c648c9a61adf939eef934a73e0cbe',
'created_at': datetime.datetime(2020, 1, 1, 0, 0, 0),
'number_of_uses': 1,
}
}
```

46 changes: 46 additions & 0 deletions docs/userguide.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,49 @@ with httpcore.ConnectionPool() as pool:
cache_pool.get("https://example.com/cachable-endpoint")
response = cache_pool.get("https://example.com/cachable-endpoint") # from the cache
```

### Temporarily Disabling the Cache

`Hishel` allows you to temporarily disable the cache for specific requests using the `cache_disabled` extension.
Per RFC9111, the cache can effectively be disabled using the `Cache-Control` headers `no-store` (which requests that the response not be added to the cache),
and `max-age=0` (which demands that any response in the cache must have 0 age - i.e. be a new request). `Hishel` respects this behavior, which can be
used in two ways. First, you can specify the headers directly:

```python
import hishel
import httpx

# With the clients
client = hishel.CacheClient()
client.get(
"https://example.com/cacheable-endpoint",
headers=[("Cache-Control", "no-store"), ("Cache-Control", "max-age=0")]
) # Ignores the cache

# With the transport
cache_transport = hishel.CacheTransport(transport=httpx.HTTPTransport())
client = httpx.Client(transport=cache_transport)
client.get(
"https://example.com/cacheable-endpoint",
headers=[("Cache-Control", "no-store"), ("Cache-Control", "max-age=0")]
) # Ignores the cache

```

Since this can be cumbersome, `Hishel` also provides some "syntactic sugar" to accomplish the same result using `HTTPX` extensions:

```python
import hishel
import httpx

# With the clients
client = hishel.CacheClient()
client.get("https://example.com/cacheable-endpoint", extensions={"cache_disabled": True}) # Ignores the cache

# With the transport
cache_transport = hishel.CacheTransport(transport=httpx.HTTPTransport())
client = httpx.Client(transport=cache_transport)
client.get("https://example.com/cacheable-endpoint", extensions={"cache_disabled": True}) # Ignores the cache

```
Both of these are entirely equivalent to specifying the headers directly.
3 changes: 3 additions & 0 deletions hishel/_async/_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ async def handle_async_request(self, request: Request) -> Response:
:rtype: httpcore.Response
"""

if request.extensions.get("cache_disabled", False):
request.headers.extend([(b"cache-control", b"no-cache"), (b"cache-control", b"max-age=0")])

key = generate_key(request)
stored_data = await self._storage.retreive(key)

Expand Down
10 changes: 10 additions & 0 deletions hishel/_async/_transports.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,16 @@ async def handle_async_request(self, request: Request) -> Response:
:return: An HTTP response
:rtype: httpx.Response
"""

if request.extensions.get("cache_disabled", False):
request.headers.update(
[
("Cache-Control", "no-store"),
("Cache-Control", "no-cache"),
*[("cache-control", value) for value in request.headers.get_list("cache-control")],
]
)

httpcore_request = httpcore.Request(
method=request.method,
url=httpcore.URL(
Expand Down
3 changes: 3 additions & 0 deletions hishel/_sync/_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ def handle_request(self, request: Request) -> Response:
:rtype: httpcore.Response
"""

if request.extensions.get("cache_disabled", False):
request.headers.extend([(b"cache-control", b"no-cache"), (b"cache-control", b"max-age=0")])

key = generate_key(request)
stored_data = self._storage.retreive(key)

Expand Down
10 changes: 10 additions & 0 deletions hishel/_sync/_transports.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,16 @@ def handle_request(self, request: Request) -> Response:
:return: An HTTP response
:rtype: httpx.Response
"""

if request.extensions.get("cache_disabled", False):
request.headers.update(
[
("Cache-Control", "no-store"),
("Cache-Control", "no-cache"),
*[("cache-control", value) for value in request.headers.get_list("cache-control")],
]
)

httpcore_request = httpcore.Request(
method=request.method,
url=httpcore.URL(
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ nav:
- Serializers: advanced/serializers.md
- Controllers: advanced/controllers.md
- HTTP Headers: advanced/http_headers.md
- Extensions: advanced/extensions.md
- Examples:
- FastAPI: examples/fastapi.md
- Flask: examples/flask.md
Expand Down
36 changes: 35 additions & 1 deletion tests/_async/test_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from httpcore._models import Request, Response

import hishel
from hishel._utils import extract_header_values, header_presents
from hishel._utils import BaseClock, extract_header_values, header_presents


@pytest.mark.anyio
Expand Down Expand Up @@ -147,3 +147,37 @@ async def test_pool_with_only_if_cached_directive_with_stored_response(use_temp_
headers=[(b"Cache-Control", b"only-if-cached")],
)
assert response.status == 504


@pytest.mark.anyio
async def test_pool_with_cache_disabled_extension(use_temp_dir):
class MockedClock(BaseClock):
def now(self) -> int:
return 1440504001 # Mon, 25 Aug 2015 12:00:01 GMT

cachable_response = httpcore.Response(
200,
headers=[
(b"Cache-Control", b"max-age=3600"),
(b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), # 1 second before the clock
],
)

async with hishel.MockAsyncConnectionPool() as pool:
pool.add_responses([cachable_response, httpcore.Response(201)])
async with hishel.AsyncCacheConnectionPool(
pool=pool, controller=hishel.Controller(clock=MockedClock())
) as cache_transport:
request = httpcore.Request("GET", "https://www.example.com")
# This should create a cache entry
await cache_transport.handle_async_request(request)
# This should return from cache
response = await cache_transport.handle_async_request(request)
assert response.extensions["from_cache"]
# This should ignore the cache
caching_disabled_request = httpcore.Request(
"GET", "https://www.example.com", extensions={"cache_disabled": True}
)
response = await cache_transport.handle_async_request(caching_disabled_request)
assert not response.extensions["from_cache"]
assert response.status == 201
35 changes: 35 additions & 0 deletions tests/_async/test_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import pytest

import hishel
from hishel._utils import BaseClock


@pytest.mark.anyio
Expand Down Expand Up @@ -165,3 +166,37 @@ async def test_transport_with_only_if_cached_directive_with_stored_response(
)
)
assert response.status_code == 504


@pytest.mark.anyio
async def test_transport_with_cache_disabled_extension(use_temp_dir):
class MockedClock(BaseClock):
def now(self) -> int:
return 1440504001 # Mon, 25 Aug 2015 12:00:01 GMT

cachable_response = httpx.Response(
200,
headers=[
(b"Cache-Control", b"max-age=3600"),
(b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), # 1 second before the clock
],
)

async with hishel.MockAsyncTransport() as transport:
transport.add_responses([cachable_response, httpx.Response(201)])
async with hishel.AsyncCacheTransport(
transport=transport, controller=hishel.Controller(clock=MockedClock())
) as cache_transport:
request = httpx.Request("GET", "https://www.example.com")
# This should create a cache entry
await cache_transport.handle_async_request(request)
# This should return from cache
response = await cache_transport.handle_async_request(request)
assert response.extensions["from_cache"]
# This should ignore the cache
caching_disabled_request = httpx.Request(
"GET", "https://www.example.com", extensions={"cache_disabled": True}
)
response = await cache_transport.handle_async_request(caching_disabled_request)
assert not response.extensions["from_cache"]
assert response.status_code == 201
36 changes: 35 additions & 1 deletion tests/_sync/test_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from httpcore._models import Request, Response

import hishel
from hishel._utils import extract_header_values, header_presents
from hishel._utils import BaseClock, extract_header_values, header_presents



Expand Down Expand Up @@ -147,3 +147,37 @@ def test_pool_with_only_if_cached_directive_with_stored_response(use_temp_dir):
headers=[(b"Cache-Control", b"only-if-cached")],
)
assert response.status == 504



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

cachable_response = httpcore.Response(
200,
headers=[
(b"Cache-Control", b"max-age=3600"),
(b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), # 1 second before the clock
],
)

with hishel.MockConnectionPool() as pool:
pool.add_responses([cachable_response, httpcore.Response(201)])
with hishel.CacheConnectionPool(
pool=pool, controller=hishel.Controller(clock=MockedClock())
) as cache_transport:
request = httpcore.Request("GET", "https://www.example.com")
# This should create a cache entry
cache_transport.handle_request(request)
# This should return from cache
response = cache_transport.handle_request(request)
assert response.extensions["from_cache"]
# This should ignore the cache
caching_disabled_request = httpcore.Request(
"GET", "https://www.example.com", extensions={"cache_disabled": True}
)
response = cache_transport.handle_request(caching_disabled_request)
assert not response.extensions["from_cache"]
assert response.status == 201
35 changes: 35 additions & 0 deletions tests/_sync/test_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import pytest

import hishel
from hishel._utils import BaseClock



Expand Down Expand Up @@ -165,3 +166,37 @@ def test_transport_with_only_if_cached_directive_with_stored_response(
)
)
assert response.status_code == 504



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

cachable_response = httpx.Response(
200,
headers=[
(b"Cache-Control", b"max-age=3600"),
(b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), # 1 second before the clock
],
)

with hishel.MockTransport() as transport:
transport.add_responses([cachable_response, httpx.Response(201)])
with hishel.CacheTransport(
transport=transport, controller=hishel.Controller(clock=MockedClock())
) as cache_transport:
request = httpx.Request("GET", "https://www.example.com")
# This should create a cache entry
cache_transport.handle_request(request)
# This should return from cache
response = cache_transport.handle_request(request)
assert response.extensions["from_cache"]
# This should ignore the cache
caching_disabled_request = httpx.Request(
"GET", "https://www.example.com", extensions={"cache_disabled": True}
)
response = cache_transport.handle_request(caching_disabled_request)
assert not response.extensions["from_cache"]
assert response.status_code == 201

0 comments on commit a6b4c65

Please sign in to comment.