Skip to main content

A fully asynchronous LRU cache decorator

Project description

Async LRU Cache

A high-performance, fully asynchronous LRU (Least Recently Used) cache decorator for Python async functions. Built specifically for asyncio applications with comprehensive caching features.

⚠️ AI slop warning:

This entire project is vibecoded! I only use it for my personal projects and I do not consider this production-ready.


✨ Features

  • 🚀 Fully Async: Built from the ground up for asyncio, no blocking operations
  • 🎯 Type Safe: Complete typing support with full IDE code completion
  • High Performance: Efficient LRU implementation with O(1) cache operations
  • 🔧 Highly Configurable: Customizable cache size, TTL, and parameter filtering
  • 📊 Detailed Statistics: Comprehensive cache performance metrics and memory usage
  • 🎛️ Flexible Control: Manual cache invalidation and clearing capabilities
  • 🛡️ Thread Safe: Proper async locking for concurrent access
  • 💾 Memory Efficient: Smart memory estimation and pretty-printed size reporting

📦 Installation

uv add async-lru-cache

Or, if you're lame:

pip install async-lru-cache

🎛️ API Reference

Decorator Parameters

Parameter Type Default Description
maxsize int 1024 Maximum number of cache entries
ttl float | None None Time-to-live in seconds (None = no expiration)
ignore_params List[str] | None None Parameter names to ignore when generating cache keys

Cache Methods

All cached functions automatically get these additional methods:

await func.get_cache_stats() -> CacheStats

Returns detailed cache statistics:

@dataclass
class CacheStats:
    current_size: int                    # Current number of cached entries
    max_size: int                       # Maximum cache size
    ttl: Optional[float]                # Time-to-live setting
    hits: int                          # Number of cache hits
    misses: int                        # Number of cache misses
    total_size_in_memory_bytes: int    # Estimated memory usage in bytes
    total_size_in_memory_pretty: str   # Human-readable memory usage

await func.cache_invalidate(*args, **kwargs) -> bool

Invalidate a specific cache entry:

@alru_cache()
async def my_func(x: int, y: str) -> str:
    return f"{x}-{y}"

# Cache an entry
result = await my_func(1, "test")

# Invalidate the specific entry
success = await my_func.cache_invalidate(1, "test")  # Returns True

await func.clear_cache() -> None

Clear all cache entries and reset statistics:

await my_func.clear_cache()
stats = await my_func.get_cache_stats()
# stats.current_size == 0, stats.hits == 0, stats.misses == 0

func.cache_info() -> Dict[str, Any]

Get cache configuration (synchronous):

info = my_func.cache_info()
# {'maxsize': 1024, 'ttl': None, 'ignore_params': []}

📚 Examples

Basic Usage with Different Data Types

from async_lru_cache import alru_cache
import asyncio
from typing import List, Dict, Any

@alru_cache(maxsize=100)
async def process_data(
    numbers: List[int], 
    config: Dict[str, Any], 
    multiplier: float = 1.0
) -> List[float]:
    """Cache works with complex data types."""
    await asyncio.sleep(0.1)  # Simulate processing
    return [n * multiplier for n in numbers]

async def example_basic():
    # Works with lists, dicts, and any hashable arguments
    result1 = await process_data([1, 2, 3], {"mode": "fast"}, 2.0)
    result2 = await process_data([1, 2, 3], {"mode": "fast"}, 2.0)  # Cache hit
    
    print(f"Results equal: {result1 == result2}")  # True
    
    stats = await process_data.get_cache_stats()
    print(f"Memory usage: {stats.total_size_in_memory_pretty}")

TTL (Time-To-Live) Caching

@alru_cache(maxsize=50, ttl=60.0)  # Cache for 60 seconds
async def fetch_stock_price(symbol: str) -> float:
    """Fetch stock price with 1-minute cache."""
    await asyncio.sleep(0.5)  # Simulate API call
    # In real scenario, fetch from API
    return 100.0 + hash(symbol) % 50

async def example_ttl():
    # First call - fetches from "API"
    price1 = await fetch_stock_price("AAPL")
    
    # Second call within 60 seconds - cache hit
    price2 = await fetch_stock_price("AAPL")
    
    # Wait for cache expiration
    await asyncio.sleep(61)
    
    # Third call - cache miss, fetches again
    price3 = await fetch_stock_price("AAPL")
    
    stats = await fetch_stock_price.get_cache_stats()
    print(f"Hits: {stats.hits}, Misses: {stats.misses}")

Ignoring Specific Parameters

@alru_cache(maxsize=100, ignore_params=["request_id", "timestamp"])
async def get_user_permissions(
    user_id: str, 
    resource: str, 
    request_id: str,
    timestamp: float
) -> List[str]:
    """Cache user permissions, ignoring request metadata."""
    await asyncio.sleep(0.2)  # Simulate database query
    return ["read", "write"] if user_id == "admin" else ["read"]

async def example_ignore_params():
    import time
    
    # These calls will hit the same cache entry despite different
    # request_id and timestamp values
    perms1 = await get_user_permissions("admin", "document", "req-1", time.time())
    perms2 = await get_user_permissions("admin", "document", "req-2", time.time() + 1)
    perms3 = await get_user_permissions("admin", "document", "req-3", time.time() + 2)
    
    # Only the first call actually executed the function
    stats = await get_user_permissions.get_cache_stats()
    print(f"Function calls: {stats.misses}")  # Should be 1

Manual Cache Invalidation

@alru_cache(maxsize=200)
async def get_user_profile(user_id: str) -> Dict[str, Any]:
    """Get user profile with manual cache control."""
    await asyncio.sleep(0.3)
    return {
        "user_id": user_id,
        "name": f"User {user_id}",
        "last_updated": time.time()
    }

async def update_user_profile(user_id: str, new_data: Dict[str, Any]):
    """Update user profile and invalidate cache."""
    # Update in database...
    await asyncio.sleep(0.1)
    
    # Invalidate the specific cache entry
    invalidated = await get_user_profile.cache_invalidate(user_id)
    print(f"Cache invalidated: {invalidated}")

async def example_invalidation():
    # Cache the profile
    profile1 = await get_user_profile("user123")
    
    # This would normally be a cache hit
    profile2 = await get_user_profile("user123")
    
    # Update profile and invalidate cache
    await update_user_profile("user123", {"name": "Updated Name"})
    
    # This will be a cache miss and fetch fresh data
    profile3 = await get_user_profile("user123")
    
    stats = await get_user_profile.get_cache_stats()
    print(f"Total calls: {stats.hits + stats.misses}")

Advanced Cache Statistics and Monitoring

@alru_cache(maxsize=1000, ttl=3600)
async def expensive_computation(data: str, iterations: int = 1000) -> str:
    """Simulate expensive computation."""
    await asyncio.sleep(iterations * 0.001)
    return f"processed_{data}_x{iterations}"

async def example_monitoring():
    # Perform various operations
    tasks = [
        expensive_computation("dataset1", 100),
        expensive_computation("dataset2", 200),
        expensive_computation("dataset1", 100),  # Cache hit
        expensive_computation("dataset3", 150),
    ]
    
    results = await asyncio.gather(*tasks)
    
    # Get detailed statistics
    stats = await expensive_computation.get_cache_stats()
    
    print("📊 Cache Statistics:")
    print(f"   Current size: {stats.current_size}/{stats.max_size}")
    print(f"   Hit rate: {stats.hits/(stats.hits + stats.misses):.2%}")
    print(f"   Memory usage: {stats.total_size_in_memory_pretty}")
    print(f"   TTL: {stats.ttl}s" if stats.ttl else "   TTL: No expiration")
    
    # Get basic cache info
    info = expensive_computation.cache_info()
    print(f"\n⚙️  Configuration:")
    print(f"   Max size: {info['maxsize']}")
    print(f"   TTL: {info['ttl']}")
    print(f"   Ignored params: {info['ignore_params']}")

Production Example: API Response Caching

import aiohttp
from async_lru_cache import alru_cache
from typing import Optional

class APIClient:
    def __init__(self):
        self.session: Optional[aiohttp.ClientSession] = None
    
    async def __aenter__(self):
        self.session = aiohttp.ClientSession()
        return self
    
    async def __aexit__(self, exc_type, exc_val, exc_tb):
        if self.session:
            await self.session.close()
    
    @alru_cache(maxsize=500, ttl=300, ignore_params=["request_timeout"])
    async def fetch_weather(
        self, 
        city: str, 
        units: str = "metric",
        request_timeout: float = 10.0
    ) -> Dict[str, Any]:
        """Fetch weather data with caching."""
        url = f"https://api.weather.com/weather/{city}"
        params = {"units": units}
        
        async with self.session.get(
            url, 
            params=params, 
            timeout=request_timeout
        ) as response:
            return await response.json()
    
    @alru_cache(maxsize=1000, ttl=3600)
    async def fetch_user_data(self, user_id: str) -> Dict[str, Any]:
        """Fetch user data with longer cache."""
        url = f"https://api.example.com/users/{user_id}"
        async with self.session.get(url) as response:
            return await response.json()
    
    async def invalidate_user_cache(self, user_id: str):
        """Invalidate user cache after updates."""
        await self.fetch_user_data.cache_invalidate(user_id)

async def production_example():
    async with APIClient() as client:
        # These calls will be cached for 5 minutes
        weather1 = await client.fetch_weather("London", "metric", 5.0)
        weather2 = await client.fetch_weather("London", "metric", 10.0)  # Cache hit!
        
        # User data cached for 1 hour
        user = await client.fetch_user_data("12345")
        
        # After user update, invalidate cache
        await client.invalidate_user_cache("12345")
        
        # Get cache statistics
        weather_stats = await client.fetch_weather.get_cache_stats()
        user_stats = await client.fetch_user_data.get_cache_stats()
        
        print(f"Weather cache: {weather_stats.hits} hits, {weather_stats.misses} misses")
        print(f"User cache: {user_stats.hits} hits, {user_stats.misses} misses")

🚀 Performance Considerations

Memory Usage

The cache automatically estimates memory usage of stored objects. For optimal performance:

  • Use reasonable maxsize values based on your memory constraints
  • Monitor memory usage with get_cache_stats()
  • Consider TTL for data that becomes stale

Concurrency

  • The cache uses async locks for thread safety
  • Multiple concurrent calls with the same arguments may result in multiple function executions
  • This is intentional to avoid blocking async operations

📄 License

This project is licensed under the MIT License - see the LICENSE file for details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

async_lru_cache-0.2.0.tar.gz (23.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

async_lru_cache-0.2.0-py3-none-any.whl (11.2 kB view details)

Uploaded Python 3

File details

Details for the file async_lru_cache-0.2.0.tar.gz.

File metadata

  • Download URL: async_lru_cache-0.2.0.tar.gz
  • Upload date:
  • Size: 23.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for async_lru_cache-0.2.0.tar.gz
Algorithm Hash digest
SHA256 2e0aaf9e3f1c750592e5875fc8f6e87ec73ad79e2932a9aa75ce4af33537f534
MD5 7346b31e207fe50035eb799e4aa72563
BLAKE2b-256 dc6921bb6e8c3dfd72e908d2b9101b5d9d0af2279d63d688d0b0095063739c9d

See more details on using hashes here.

Provenance

The following attestation bundles were made for async_lru_cache-0.2.0.tar.gz:

Publisher: publish-to-pypi.yml on AetherMagee/async-lru-cache

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file async_lru_cache-0.2.0-py3-none-any.whl.

File metadata

File hashes

Hashes for async_lru_cache-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f15edbd00c7212845387899c0d1b9e6c79702d6d814f61589fdea3a308713bd9
MD5 60d6d47c3a5deb0963ef605f29378e2f
BLAKE2b-256 5ca89575789d5ab8d500d517aaaa91fb1929f532c6a3e76e4b3a7a7f23e27f0c

See more details on using hashes here.

Provenance

The following attestation bundles were made for async_lru_cache-0.2.0-py3-none-any.whl:

Publisher: publish-to-pypi.yml on AetherMagee/async-lru-cache

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page