Hishel HTTP Caching

repository·master·Indexed 19 days ago

https://github.com/karpetrosyan/hishel

An RFC 9111 compliant HTTP caching library for Python. Hishel provides type-safe and flexible caching for HTTP clients like HTTPX and Requests, as well as ASGI web frameworks including FastAPI, Starlette, Litestar, and BlackSheep. It supports both synchronous and asynchronous workflows, features a SQLite storage backend, and enables caching for GraphQL queries via body-sensitive content caching.

Tokens
23K
Snippets
69
Records
82
Agent score
63%

What's inside Hishel

  1. Overview of Hishel HTTP Caching

    master

    Hishel is a modern HTTP caching library for Python that implements the RFC 9111 specifications. It is designed to provide seamless caching integration for popular HTTP clients and web frameworks with minimal code changes.

    Key capabilities include:

    • RFC 9111 Compliance: Adheres to the latest HTTP caching standards.
    • Broad Integration: Supports HTTPX, Requests, ASGI, FastAPI, and BlackSheep.
    • Flexible Storage: Currently provides a SQLite backend.
    • Async & Sync Support: Works with both synchronous and asynchronous workflows.
    • Universal ASGI Support: Compatible with any ASGI application (e.g., Starlette, Litestar, BlackSheep).
    • GraphQL Support: Enables caching of GraphQL queries using body-sensitive content caching.
    • Memory Efficiency: Supports streaming to prevent loading large payloads into memory.
  2. Use the Sans-IO state machine for HTTP caching

    master

    Hishel provides a pure-Python, RFC 9111-compliant state machine for HTTP caching. Instead of managing cache logic manually, you interact with a state machine that dictates the next required action, the inputs needed for state transitions, and the meaning of the current state.

    To use the state machine, you must follow these rules:

    1. Initialization: Start by creating an IdleClient state. Do not manually instantiate other states.
    2. Transitions: Use the .next() method on the current state to transition to the next state. The .next() method is fully typed and returns a new state object.
    3. State-Specific Logic: Each state (e.g., CacheMiss, FromCache, NeedRevalidation) has its own properties and a .next() method signature tailored to the specific inputs required for that stage of the HTTP lifecycle.
    from hishel import IdleClient, Request
    
    # 1. Initialize the state machine
    state = IdleClient()
    
    # 2. Transition to the next state using .next()
    request = Request(url="https://example.com")
    next_state = state.next(request, associated_entries=[])
    
    # next_state will now be a specific state type like CacheMiss, 
    # FromCache, or NeedRevalidation
  3. How async/sync code generation works in Hishel

    master

    Hishel uses an unasync strategy to provide both async and sync APIs without duplicating code.

    The Workflow

    1. Write async code once: All shared functionality is written in async files (e.g., hishel/_core/_storages/_async_*.py or tests/_core/_async/*.py).
    2. Automatic transformation: The scripts/unasync script transforms the async code into synchronous versions (e.g., hishel/_core/_storages/_sync_*.py).

    Critical Rules

    • DO: Write and edit async files only (_async_*.py).
    • DO: Run ./scripts/fix before committing to ensure sync files are updated.
    • DON'T: Manually edit synchronous files (_sync_*.py).
    • DON'T: Commit async changes without running unasync.
    • DON'T: Modify sync test files directly.
    # Async code (you write this)
    async def store(self, key: str) -> None:
        async with self.connection as conn:
            await conn.execute(...)
    
    # Sync code (automatically generated)
    def store(self, key: str) -> None:
        with self.connection as conn:
            conn.execute(...)
  4. How custom storage backends work

    master
    If you need a storage backend other than SQLite or Redis, you can implement the base storage interface. Once implemented, your custom storage can be used with AsyncCacheProxy, SyncCacheProxy, or any other Hishel integration that accepts a storage object.
  5. Inspect Response Metadata to monitor cache operations

    master

    Hishel provides read-only metadata on responses to inspect cache operations. These fields are prefixed with hishel_.

    Available Response Metadata

    • hishel_from_cache (bool | None): True if the response was served from cache; False if fetched from the origin server.
    • hishel_revalidated (bool | None): True if a stale cached response was revalidated with the origin server (e.g., resulting in a 304 Not Modified).
    • hishel_stored (bool | None): True if the response was successfully saved to the cache. False if it was not cacheable (e.g., due to Cache-Control: no-store).
    • hishel_created_at (float | None): A POSIX timestamp indicating when the response entry was created in the cache.

    Accessing Metadata

    For HTTPX: Access via response.extensions.get("field_name").

    For Requests: Access via response.headers.get("X-Hishel-Field-Name") (note the header mapping).

    from hishel.httpx import SyncCacheClient
    
    client = SyncCacheClient()
    response = client.get("https://api.example.com/data")
    
    if response.extensions.get("hishel_from_cache"):
        print("✓ Cache hit")
    
    if response.extensions.get("hishel_revalidated"):
        print("Response was revalidated (304 Not Modified)")
    
    if response.extensions.get("hishel_stored"):
        print("✓ Response stored in cache")
    
    created = response.extensions.get("hishel_created_at")
    if created:
        print("Cached at:", created)
  6. Configure per-request TTL in RedisStorage

    master

    While RedisStorage has a default ttl, you can override this for specific requests by adding a hishel_ttl key to the request's metadata. The storage will prioritize this metadata value over the default TTL.

    # Example of setting a custom TTL for a specific request
    request.metadata["hishel_ttl"] = 500
    # When this request is stored in RedisStorage, it will use 500s instead of the default
  7. Implement custom caching logic with FilterPolicy

    master

    The FilterPolicy allows you to implement custom caching logic by applying user-defined filters to requests and responses. This is useful for decisions that cannot be made based on HTTP headers alone (e.g., inspecting the response body or URL patterns).

    How it works

    • Request Filters: All filters in request_filters must return True for a request to be checked against the cache.
    • Response Filters: All filters in response_filters must return True for a response to be stored in the cache.
    • Filters are applied in sequence.
    from hishel import FilterPolicy, BaseFilter, Request, Response
    
    policy = FilterPolicy(
        request_filters=[...],   # List of request filters
        response_filters=[...],  # List of response filters
    )
  8. Configure Request Metadata to control caching

    master

    You can control how Hishel caches requests by setting specific metadata. All metadata fields are prefixed with hishel_ to avoid collisions.

    Available Request Metadata

    • hishel_ttl (float | None): Sets a custom time-to-live (TTL) in seconds for the cached response. Overrides the storage's default_ttl.
    • hishel_refresh_ttl_on_access (bool | None): If True, accessing a cached entry resets its TTL (sliding expiration). If False, the TTL countdown is fixed from the original storage time.
    • hishel_body_key (bool | None): If True, includes the request body in the cache key generation. This is essential for caching POST requests or GraphQL queries where the same URL might have different payloads.

    How to set metadata

    For HTTPX:

    • Use the extensions parameter in your request (recommended).
    • Or use X-Hishel-* headers.

    For Requests:

    • Use X-Hishel-* headers.
    from hishel.httpx import SyncCacheClient
    
    client = SyncCacheClient()
    
    response = client.get(
        "https://api.example.com/data",
        extensions={"hishel_ttl": 3600, "hishel_refresh_ttl_on_access": True, "hishel_body_key": True}
    )
  9. Use SpecificationPolicy for RFC 9111 compliant caching

    master

    The SpecificationPolicy is the default caching policy in Hishel. It implements HTTP caching according to RFC 9111. You can configure its behavior using a CacheOptions object to control whether the cache is shared or private, which HTTP methods are cached, and whether stale responses are allowed to be served.

    from hishel import CacheOptions, SpecificationPolicy
    
    policy = SpecificationPolicy(
        cache_options=CacheOptions(
            shared=True,           # Act as a shared cache (proxy/CDN)
            allow_stale=False,     # Don't serve stale responses
            supported_methods=["GET", "HEAD"],  # Cache these methods
        )
    )
  10. Quick Start: Synchronous Zapros with caching

    master

    To use caching in a synchronous Zapros client, wrap a StdNetworkHandler with CacheMiddleware and pass it to the Client constructor. You can inspect the caching status of a response using response.context.get("caching").

    from zapros import Client, CacheMiddleware, StdNetworkHandler
    
    client = Client(handler=CacheMiddleware(StdNetworkHandler()))
    
    response = client.get("https://api.example.com/data")
    print(response.context.get("caching"))  # {'from_cache': False, ...}
    
    response = client.get("https://api.example.com/data")
    print(response.context.get("caching"))  # {'from_cache': True, ...}
  11. Integrate Hishel policies with HTTPX, Requests, or ASGI

    master

    Hishel policies can be applied across different HTTP clients and frameworks:

    HTTPX (Async)

    import httpx
    from hishel import AsyncCacheClient, SpecificationPolicy, CacheOptions
    
    policy = SpecificationPolicy(cache_options=CacheOptions(shared=False))
    async with AsyncCacheClient(policy=policy) as client:
        response = await client.get("https://api.example.com/data")

    HTTPX (Sync)

    import httpx
    from hishel import SyncCacheClient, SpecificationPolicy, CacheOptions
    
    policy = SpecificationPolicy(cache_options=CacheOptions(shared=True))
    with SyncCacheClient(policy=policy) as client:
        response = client.get("https://api.example.com/data")

    Requests

    import requests
    from hishel.requests import CacheAdapter
    from hishel import SpecificationPolicy, CacheOptions
    
    policy = SpecificationPolicy(cache_options=CacheOptions(shared=False))
    session = requests.Session()
    session.mount("https://", CacheAdapter(policy=policy))
    session.mount("http://", CacheAdapter(policy=policy))
    
    response = session.get("https://api.example.com/data")

    ASGI Middleware

    from hishel.asgi import ASGICacheMiddleware
    from hishel import SpecificationPolicy, CacheOptions
    
    policy = SpecificationPolicy(cache_options=CacheOptions(shared=True))
    app = ASGICacheMiddleware(app=your_asgi_app, policy=policy)
  12. Use hishel with HTTPX

    master

    You can use AsyncCacheClient from hishel.httpx to provide caching capabilities to an HTTPX client. The first request to a URL will fetch from the origin, and subsequent requests will be served from the cache.

    from hishel.httpx import AsyncCacheClient
    
    async with AsyncCacheClient() as client:
        # First request - fetches from origin
        await client.get("https://hishel.com")
        # Second request - served from cache
        response = await client.get("https://hishel.com")
        print(response.text)