Persistent cache implementation for httpx and httpcore following RFC 9111 specification
74
Mock connection pools and transports for unit testing HTTP caching behavior. These utilities allow you to test caching logic without making actual network requests.
Mock transport for testing synchronous HTTP client caching behavior.
class MockTransport(httpx.BaseTransport):
def handle_request(self, request: httpx.Request) -> httpx.Response:
"""Handle request by returning pre-configured mock response"""
def add_responses(self, responses: list[httpx.Response]) -> None:
"""
Add mock responses to be returned in order.
Parameters:
- responses: List of httpx.Response objects to return
"""Usage Examples:
import httpx
import hishel
# Create mock responses
response1 = httpx.Response(200, json={"data": "first"})
response2 = httpx.Response(200, json={"data": "second"})
# Set up mock transport
mock_transport = hishel.MockTransport()
mock_transport.add_responses([response1, response2])
# Use with cache client for testing
cache_transport = hishel.CacheTransport(transport=mock_transport)
with httpx.Client(transport=cache_transport) as client:
# First request uses first mock response
resp1 = client.get("https://api.example.com/data")
assert resp1.json() == {"data": "first"}
# Second request should be served from cache
resp2 = client.get("https://api.example.com/data")
assert resp2.json() == {"data": "first"} # Same as first (cached)Mock transport for testing asynchronous HTTP client caching behavior.
class MockAsyncTransport(httpx.AsyncBaseTransport):
async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
"""Handle async request by returning pre-configured mock response"""
def add_responses(self, responses: list[httpx.Response]) -> None:
"""
Add mock responses to be returned in order.
Parameters:
- responses: List of httpx.Response objects to return
"""Usage Examples:
import httpx
import hishel
import asyncio
async def test_async_caching():
# Create mock responses with cache headers
response = httpx.Response(
200,
headers={"cache-control": "max-age=3600"},
json={"data": "cached"}
)
# Set up mock async transport
mock_transport = hishel.MockAsyncTransport()
mock_transport.add_responses([response])
# Use with async cache client
cache_transport = hishel.AsyncCacheTransport(transport=mock_transport)
async with httpx.AsyncClient(transport=cache_transport) as client:
# First request uses mock response
resp1 = await client.get("https://api.example.com/data")
assert resp1.json() == {"data": "cached"}
# Second request served from cache (no additional mock response needed)
resp2 = await client.get("https://api.example.com/data")
assert resp2.json() == {"data": "cached"}
asyncio.run(test_async_caching())Lower-level mock connection pools for testing httpcore-level caching.
class MockConnectionPool:
def handle_request(self, request: httpcore.Request) -> httpcore.Response:
"""Handle httpcore request with mock response"""
def add_responses(self, responses: list[httpcore.Response]) -> None:
"""
Add mock httpcore responses.
Parameters:
- responses: List of httpcore.Response objects
"""
class MockAsyncConnectionPool:
async def handle_async_request(self, request: httpcore.Request) -> httpcore.Response:
"""Handle async httpcore request with mock response"""
def add_responses(self, responses: list[httpcore.Response]) -> None:
"""
Add mock httpcore responses.
Parameters:
- responses: List of httpcore.Response objects
"""Usage Examples:
import httpcore
import hishel
# Create mock httpcore responses
response = httpcore.Response(
200,
headers=[(b"content-type", b"application/json")],
content=b'{"data": "test"}'
)
# Test with mock connection pool
mock_pool = hishel.MockConnectionPool()
mock_pool.add_responses([response])
# Use for lower-level testing
request = httpcore.Request(b"GET", b"https://api.example.com/data")
response = mock_pool.handle_request(request)
assert response.status == 200import httpx
import hishel
import pytest
def test_cache_hit_miss():
# Mock response with cache headers
cached_response = httpx.Response(
200,
headers={"cache-control": "max-age=3600", "etag": '"abc123"'},
json={"data": "original"}
)
# Fresh response for revalidation
fresh_response = httpx.Response(
200,
headers={"cache-control": "max-age=3600", "etag": '"def456"'},
json={"data": "updated"}
)
mock_transport = hishel.MockTransport()
mock_transport.add_responses([cached_response, fresh_response])
storage = hishel.InMemoryStorage()
cache_transport = hishel.CacheTransport(
transport=mock_transport,
storage=storage
)
with httpx.Client(transport=cache_transport) as client:
# First request - cache miss
resp1 = client.get("https://api.example.com/data")
assert resp1.json() == {"data": "original"}
# Second request - cache hit
resp2 = client.get("https://api.example.com/data")
assert resp2.json() == {"data": "original"} # Served from cache
# Verify only one network request was made
# (second response in mock is unused)import httpx
import hishel
def test_cache_revalidation():
# Original response
original_response = httpx.Response(
200,
headers={"cache-control": "max-age=0", "etag": '"version1"'},
json={"version": 1}
)
# 304 Not Modified response
not_modified_response = httpx.Response(
304,
headers={"cache-control": "max-age=3600", "etag": '"version1"'}
)
mock_transport = hishel.MockTransport()
mock_transport.add_responses([original_response, not_modified_response])
cache_transport = hishel.CacheTransport(transport=mock_transport)
with httpx.Client(transport=cache_transport) as client:
# First request
resp1 = client.get("https://api.example.com/data")
assert resp1.json() == {"version": 1}
# Second request triggers revalidation due to max-age=0
resp2 = client.get("https://api.example.com/data")
assert resp2.json() == {"version": 1} # Same content (304 response)
assert resp2.status_code == 200 # But status is 200 (from cache)import httpx
import hishel
import tempfile
import pytest
@pytest.mark.parametrize("storage_class", [
hishel.InMemoryStorage,
hishel.FileStorage,
])
def test_storage_backends(storage_class):
response = httpx.Response(
200,
headers={"cache-control": "max-age=3600"},
json={"backend": storage_class.__name__}
)
mock_transport = hishel.MockTransport()
mock_transport.add_responses([response])
# Configure storage based on type
if storage_class == hishel.FileStorage:
with tempfile.TemporaryDirectory() as tmpdir:
storage = storage_class(base_path=tmpdir)
else:
storage = storage_class()
cache_transport = hishel.CacheTransport(
transport=mock_transport,
storage=storage
)
with httpx.Client(transport=cache_transport) as client:
resp1 = client.get("https://api.example.com/data")
resp2 = client.get("https://api.example.com/data") # From cache
assert resp1.json() == resp2.json()import httpx
import hishel
def test_no_cache_directive():
# Response with no-cache directive
response = httpx.Response(
200,
headers={"cache-control": "no-cache"},
json={"directive": "no-cache"}
)
revalidation_response = httpx.Response(
200,
headers={"cache-control": "max-age=3600"},
json={"directive": "revalidated"}
)
mock_transport = hishel.MockTransport()
mock_transport.add_responses([response, revalidation_response])
cache_transport = hishel.CacheTransport(transport=mock_transport)
with httpx.Client(transport=cache_transport) as client:
# First request
resp1 = client.get("https://api.example.com/data")
assert resp1.json() == {"directive": "no-cache"}
# Second request should revalidate due to no-cache
resp2 = client.get("https://api.example.com/data")
assert resp2.json() == {"directive": "revalidated"}import hishel
class TestStorage(hishel.BaseStorage):
"""Test storage that tracks operations"""
def __init__(self):
super().__init__()
self.stored_keys = []
self.retrieved_keys = []
self.cache = {}
def store(self, key, response, request, metadata=None):
self.stored_keys.append(key)
self.cache[key] = (response, request, metadata)
def retrieve(self, key):
self.retrieved_keys.append(key)
return self.cache.get(key)
def remove(self, key):
self.cache.pop(key, None)
def update_metadata(self, key, response, request, metadata):
if key in self.cache:
self.cache[key] = (response, request, metadata)
def close(self):
pass
# Use in tests
def test_with_custom_storage():
test_storage = TestStorage()
response = httpx.Response(200, json={"test": True})
mock_transport = hishel.MockTransport()
mock_transport.add_responses([response])
cache_transport = hishel.CacheTransport(
transport=mock_transport,
storage=test_storage
)
with httpx.Client(transport=cache_transport) as client:
client.get("https://api.example.com/data")
client.get("https://api.example.com/data") # Cache hit
assert len(test_storage.stored_keys) == 1
assert len(test_storage.retrieved_keys) == 1Install with Tessl CLI
npx tessl i tessl/pypi-hisheldocs
evals
scenario-1
scenario-2
scenario-3
scenario-4
scenario-5
scenario-6
scenario-7
scenario-8
scenario-9
scenario-10