cashews

repository·master·Indexed 20 days ago

https://github.com/krukov/cashews

An asynchronous cache framework for building fast and reliable applications. It supports multiple backends including in-memory, Redis, and DiskCache, and provides a decorator-based API for caching coroutines. Features include advanced caching strategies (failover, early, soft, circuit breaker), a tag-based invalidation system, transactional mode for atomicity, and support for Bloom filters.

Tokens
5.5K
Snippets
21
Records
21
Agent score
20%

What's inside cashews

  1. Configure Cache Keys and Templates

    master

    Cashews generates keys automatically using the function name, module, and arguments. You can customize this using the key parameter in decorators.

    Key Templating Features

    • Attribute Access: Access object attributes via {obj.attr}.
    • Built-in Formatters: Use {val:lower}, {val:upper}, {val:len}, {val:jwt}, or {val:hash(algo)} (e.g., sha1, md5).
    • Custom Formatters: Register functions via default_formatter.register("name").
    • Type Formatters: Register specific handlers for types via default_formatter.type_format(Type).
    • Context Variables: Use {@:get(var_name)} to inject variables from a key_context.

    Handling self in Class Methods

    When decorating class methods, self is included in the key, which often leads to undesirable results. Solutions include:

    1. Defining __str__ on the class.
    2. Providing an explicit key template that uses self attributes.
    3. Using @noself(cache) or @noself_cache to exclude the instance from the key.
    from cashews import cache, default_formatter, key_context
    
    # Using built-in formatters
    @cache(ttl="2h", key="user_info:{user.name:lower}:{password:hash(sha1)}")
    async def get_user_info(user, password):
        ...
    
    # Using context variables
    @cache(ttl="2h", key="user:{@:get(client_id)}")
    async def get_current_user():
        pass
    
    with key_context(client_id=135356):
        await get_current_user()
    
    # Custom type formatter
    from decimal import Decimal
    @default_formatter.type_format(Decimal)
    def _decimal(value: Decimal) -> str:
        return str(value.quantize(Decimal("0.00")))
  2. Cache Invalidation and Tags

    master

    You can invalidate cache entries using patterns or tags.

    Pattern Invalidation

    Use @cache.invalidate("pattern:*") to clear keys matching a specific pattern. Warning: On Redis, this performs a full database scan and can be slow.

    Tag System

    To avoid performance issues with pattern scanning, use Tags. Tags store related keys in a separate SET, allowing for efficient bulk invalidation.

    • Tag keys during setup: @cache(..., tags=["tag1", "tag2"]).
    • Invalidate tags: await cache.delete_tags("tag1").
    • Invalidate specific tag instances: await cache.delete_tags("page:1").

    Invalidate Further

    Use the invalidate_further() context manager to ensure that subsequent calls to a cached function are invalidated immediately (useful when a function call triggers a side effect that changes data).

    from cashews import cache
    
    # Tagging keys
    @cache(ttl="1h", tags=["items", "page:{page}"])
    async def items(page=1):
        ...
    
    # Invalidate by tag
    await cache.delete_tags("items")
    await cache.delete_tags("page:1")
    
    # Invalidate future calls via context manager
    from cashews import invalidate_further
    
    async def add_item(item):
        with invalidate_further():
            await items()
  3. Transactional Mode

    master

    Cashews supports transactional operations to ensure atomicity and prevent race conditions.

    Supported operations: set, set_many, delete, delete_many, delete_match, and incr.

    Isolation Modes

    • fast: Memory-based, low overhead (0-7%), but does not protect against race conditions; used for atomicity.
    • locked (Default): Uses shared locks per key (4-9% overhead). Protects against race conditions.
    • serializable: Uses a global shared lock (7-50% overhead). Only one transaction at a time.
    from cashews import cache, TransactionMode
    
    # Using a decorator
    @cache.transaction(TransactionMode.LOCKED) 
    async def my_handler():
        await cache.set("key", "value")
    
    # Using a context manager
    async def login():
        async with cache.transaction() as tx:
            await cache.incr("count")
            await cache.set("session", "data")
            # tx.rollback() can be called here
  4. Cache Strategies (Decorators)

    master

    Cashews provides several decorators to implement advanced caching patterns for coroutines:

    • @cache(ttl=..., key=...): Simple cache. Executes, stores, and returns from cache until expiration.
    • @cache.failover(ttl=..., exceptions=(...)): Returns cached result if the decorated function raises one of the specified exceptions.
    • @cache.hit(ttl=..., cache_hits=..., update_after=...): Expires cache after a specific number of hits.
    • @cache.early(ttl=..., early_ttl=...): Solves cache stampede by recalculating the result in the background when the cache is near expiration (early_ttl).
    • @cache.soft(ttl=..., soft_ttl=...): Provides fail protection. If recalculation fails, returns the old value if it is within the soft_ttl window.
    • @cache.iterator(ttl=..., key=...): Specifically for caching async iterators.
    • @cache.locked(ttl=...): Prevents cache stampede by locking function calls until the first one finishes. Note: This does not cache the result unless lock=True is passed to @cache().
    • @cache.rate_limit(limit=..., period=..., ttl=...): Limits function calls; raises RateLimitError if exceeded.
    • @cache.circuit_breaker(errors_rate=..., period=..., ttl=..., half_open_ttl=...): Implements the circuit breaker pattern based on error rates.
    • @cache.bloom(capacity=..., false_positives=...): Experimental Bloom filter for membership testing.
    from datetime import timedelta
    from cashews import cache
    
    # Simple cache with custom key
    @cache(ttl=timedelta(hours=3), key="user:{request.user.uid}")
    async def long_running_function(request):
        ...
    
    # Failover cache
    @cache.failover(ttl="2h", exceptions=(ValueError, MyException))
    async def get_status(name):
        ...
    
    # Early cache (background refresh)
    @cache.early(ttl="10m", early_ttl="7m")
    async def get(name):
        ...
  5. Set up the Cashews development environment

    master

    To set up the development environment, clone the repository and install pre-commit within a virtual environment to manage git hooks.

    pip install pre-commit && pre-commit install --install-hooks
  6. Configure the cache with cache.setup()

    master

    Use cache.setup() to configure the backend for the global cache instance. Supported protocols include mem:// for in-memory caching, as well as Redis and DiskCache (syntax depends on the specific backend requirements).

    from cashews import cache
    
    cache.setup("mem://")  # configure as in-memory cache
  7. Run tests using pytest

    master

    Alternatively, you can use pytest. Note that when running pytest directly, two tests are expected to fail, which is normal behavior.

    First, install the project with the necessary extra dependencies:

    pip install .[tests,redis,diskcache,speedup] fastapi aiohttp requests httpx SQLAlchemy prometheus-client

    Then run the tests:

    • pytest: Runs all tests with all backends.
    • pytest -m "not redis": Runs all tests except those requiring the Redis backend.
    pip install .[tests,redis,diskcache,speedup] fastapi aiohttp requests httpx SQLAlchemy prometheus-client
    
    pytest
    pytest -m "not redis"
  8. Install cashews

    master

    Install the core library using pip. You can also install optional dependencies for specific backends or features like Redis, DiskCache, Dill (for advanced object serialization in Redis), or Speedup (for Bloom filters).

    pip install cashews
    pip install cashews[redis]
    pip install cashews[diskcache]
    pip install cashews[dill] # can cache in redis more types of objects
    pip install cashews[speedup] # for bloom filters
  9. Configure the Cashews cache

    master

    You can configure the default cashews cache using a connection URL or by passing keyword arguments to cache.setup(). You can also create independent cache instances using the Cache class.

    To use different backends for different keys, provide a prefix during setup. Cashews will route requests to the appropriate backend based on the key's prefix.

    from cashews import cache, Cache
    
    # via url
    cache.setup("redis://0.0.0.0/?db=1&socket_connect_timeout=0.5&suppress=0&secret=my_secret&enable=1")
    
    # or via kwargs
    cache.setup("redis://0.0.0.0/", db=1, wait_for_connection_timeout=0.5, suppress=False, secret=b"my_key", enable=True)
    
    # Create a custom instance
    my_cache = Cache()
    my_cache.setup("mem://")
    
    # Setup different backends based on prefix
    cache.setup("redis://redis/0")
    cache.setup("mem://?size=500", prefix="user")
    
    await cache.get("accounts")  # uses redis
    await cache.get("user:1")    # uses memory
  10. Run tests using tox

    master

    Cashews uses tox to manage test environments for different backends. You can run specific test suites depending on the backend you are testing:

    • tox -e py: Tests for the inmemory backend.
    • tox -e py-diskcache: Tests for the diskcache backend.
    • tox -e py-redis: Tests for the redis backend (requires a running Redis instance).
    • tox -e py-redis_cluster: Tests for the redis cluster backend (requires a running Redis cluster).
    • tox -e py-integration: Tests for integrations with aiohttp and fastapi.
    • tox: Runs all tests for all installed Python versions.
    pip install tox
    tox -e py
    tox -e py-diskcache
    tox -e py-redis
    tox -e py-redis_cluster
    tox -e py-integration
    tox
  11. Use the decorator-based API

    master

    You can easily cache the results of asynchronous functions using the @cache decorator. You can specify the Time-To-Live (ttl) using strings (e.g., "3h") and define dynamic keys using a key parameter that can reference function arguments.

    from cashews import cache
    
    cache.setup("mem://")
    
    @cache(ttl="3h", key="user:{request.user.uid}")
    async def long_running_function(request):
        ...
  12. Use the functional API for fine-grained control

    master

    For more control over when and how data is cached, use the cache.set() method directly within your functions. This allows you to manually manage keys and expiration times.

    from cashews import cache
    
    cache.setup("mem://")
    
    async def cache_using_function(request):
        await cache.set(key=request.user.uid, value=request.user, expire="20h")
        ...