-
Notifications
You must be signed in to change notification settings - Fork 25
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
Add cache_disabled context manager #105
Conversation
Added one more update here. Now the This new commit fixes that issue, and now you could call infinite import hishel
with hishel.CacheClient() as client:
with client.cache_disabled():
with client.cache_disabled():
with client.cache_disabled():
response = client.get("https://www.example.com") # Works! |
I'm wondering if we really need this change. Including this in the Also, as stated in https://hishel.com/userguide/#clients-and-transports, we do not recommend using the Instead, we can use the functionality that no-store and max-age request directivesIf the no-store request directive is present in the request headers, the cache will not save the response. no-store: RFC Docs import hishel
cl = hishel.CacheClient()
cl.get("https://hishel.com", headers=[('Cache-Control', 'no-store')])
cl.get("https://hishel.com").extensions['from_cache'] # False Also, we don't just want to avoid storing the incoming response in the cache. We also want to ignore the existing response that is in cache, so here's how we can do it: cl.get("https://hishel.com", headers=[('Cache-Control', 'no-store', 'max-age=0')]) The no-store request directive prevents the response from being saved into the cache, and the max-age directive prevents the response from being taken from the cache. |
Hey! If this isn't the right fit for this library, no worries! I've implemented this feature in a subclass of CacheClient and AsyncCacheClient are Probably The Most Popular, If Not Preferred, API'sThat said, I think the reality of most people coming to Hishel is that they've discovered httpx, and they want a caching layer, kind of like requests-cache or cache-control. HTTP clients are prolific in python development, and most python devs don't have a fine-grained knowledge of RFC9111 (or even that it exists!). They just want to be kinder to API's they use and/or limit bandwidth use. Both # requests-cache
from requests_cache import CachedSession
session = CachedSession()
# cache-control
import requests
from cachecontrol import CacheControl
session = CacheControl(requests.Session()) So, I think This Method Makes Turning The Cache On And Off Dead SimpleIn any case, I think this change actually makes a lot of sense - even with the transports. Think about it this way - you can use Hishel in two main ways: # Using Hishel as an on-by-default cache client with exceptions:
from hishel import CacheClient
client = CacheClient
response = client.get("https://www.example.com") # Uses Cache
with client.cache_disabled():
response = client.get("https://www.example.com") # Temporarily disables cache.
# Using Hishel as an off-by-default cache client with exceptions
from httpx import Client
from hishel import CacheTransport
cached_transport = CacheTransport(transport=httpx.HTTPTransport())
client = Client()
response = client.get("https://www.example.com") # No cache
client._transport = cached_transport
response = client.get("https://www.example.com") # Cached People Probably Don't Know About Cache DirectivesSelf-explanatory in view of the above. I didn't know about the ConclusionAgain, thanks so much for maintaining this library! It's been great, and I'm happy to use it in my projects. No hard feelings if you reject this PR. But for the Thanks! |
Thank you very much for this pull request. I appreciate you being involved in this library 🙏
You mentioned a very important thing here. I wish I could make them more usable. But not everyone knows how to use custom transports, and it is much easier to use the I even overrided some protected methods in So I am happy to merge this PR as a I find that it could be useful to provide a high-level interface for low-level things, like cache-control directives, so if we want to include something like this in our CacheClient class, I wish it could be just a simplified API for the RFC-level things. |
Hey! Maybe there's a better way to do this for hishel that matches the structure of httpx a bit better. What if the API was something like this? from hishel import CacheClient
client = CacheClient()
# Do this
client.get("https://www.example.com", extensions={"cache_disabled": True})
# As a convenience extension for this
client.get("https://www.example.com", headers=[('Cache-Control', 'no-store', 'max-age=0')]) Then, you could implement some logic in the Thoughts? |
One more thought - I think the core goal of this change is to make something in the code that looks like "disable the cache, please!" so that the python code is understandable and maintainable, which the RFC-compliant headers are not. So another way to do it would be something like this: # importable from hishel
NO_SAVE = {"Cache-Control": "no-store"}
CACHE_DISABLED = {"Cache-control": ["no-store", "max-age=0"]}
import hishel
client = hishel.CacheClient()
# Disable cache using named headers
client.get("https://www.example.com", headers=CACHE_DISABLED)
# Disable saving using named headers
client.get("https://www.example.com", headers=NO_SAVE)
# Adding additional headers (using PEP 584 syntax, but any dict merging technique could be used)
client.get("https:/www.example.com", headers=CACHE_DISABLED | {"header": "value"})) Honestly, at this point, I think this is how I'm going to implement this feature in my own code. But it still might be nice to either have it in Hishel's code base, or suggested somewhere in the documentation (like a cookbook or something). |
Hi! These solutions look really great, though I like the first one more. We can have a section in the documentation that contains all about the usage of request and response extensions such as "from_cache", "cache_metadata" and "cache_disable". Also, I just realized that we do not have any documentation about the existing extensions that Hishel supports, so I think it's a good opportunity to tidy up extensions and also introduce a new one. |
Awesome! Question for you - can you write a test for the transports with a cacheable response that returns a result from the cache? I'm working on a new PR for this, but I'm running into difficulty writing a test for the transport that returns a mocked response from the cache. With that, I should be able to implement the logic in the transports to use the extensions feature mentioned above. Thanks! Parker |
Yeah, of course. |
Sure! Here is what I'm trying (and failing) at. I'm sure the problem is more obvious to you: def test_transport_with_cache_disabled_extension(
use_temp_dir,
):
with hishel.MockTransport() as transport:
transport.add_responses(
[
httpx.Response(200,),
httpx.Response(200,),
]
)
with hishel.CacheTransport(transport=transport) 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
response = cache_transport.handle_request(request, extensions={"cache_disabled": True})
assert not response.extensions["from_cache"] Test should fail. When I write the code to add the appropriate headers, it should pass. One more thought for your consideration. Would you prefer a single "extension" for "cache_disabled" that can be set true or false (which is all I'm looking for), or maybe a "cache_mode" that takes a string or enumerable that could be "disabled," "no_store", or "enabled" (the default?). Thanks! |
Here is an example of how you can mock the caching responses. from hishel._utils import BaseClock
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", headers={"Cache-Control": "max-age=0"}
)
response = cache_transport.handle_request(caching_disabled_request)
assert not response.extensions["from_cache"]
assert response.status_code == 201 Also, there is a test in the controllers test that uses such an approach. hishel/tests/test_controller.py Lines 240 to 257 in 893690f
Very good question, though I think it should be up to you as the author of this feature. |
I'm a recent
httpx
convert fromrequests
, and I love the excellent requests-cache library. One of the features I miss the most fromhishel
(and use quite frequently withrequests-cache
) is the ability to use the.cache_disabled
context manager, documented here.After getting a bit more comfortable with this code base in my last PR, I wanted to submit a new one to add this feature into the library. This PR includes:
CacheClient
andAsyncCacheClient