๐พ Caching
Clientele provides built-in support for caching HTTP GET requests using the @memoize decorator.
Quick Start
from clientele import api, cache
client = api.APIClient(base_url="https://api.example.com")
@cache.memoize(ttl=300) # Cache for 5 minutes
@client.get("/users/{id}")
def get_user(id: int, result: dict) -> dict:
return result
# First call - hits the API
user = get_user(id=123)
# Second call - returns cached result
user = get_user(id=123) # No HTTP request made!
How It Works
The @memoize decorator:
- Extracts request context from the underlying
@client.get()decorator - Generates cache keys from the HTTP method, path template, and function parameters
- Checks the cache before executing the HTTP request
- Stores results with optional TTL (time-to-live) expiration
- Respects LRU eviction when the cache reaches its maximum size
GET Requests Only
Only use @memoize with GET requests (idempotent operations). POST/PUT/PATCH/DELETE should not be cached as they modify server state.
Configuration
Basic TTL (Time-To-Live)
Set how long cached responses remain valid:
@memoize(ttl=300) # 5 minutes
@client.get("/pokemon/{id}")
def get_pokemon(id: int, result: dict) -> dict:
return result
Custom Cache Keys
Override the default key generation with a custom function:
@memoize(
ttl=600,
key=lambda user_id: f"user:{user_id}" # Custom key format
)
@client.get("/users/{user_id}")
def get_user(user_id: int, result: dict) -> dict:
return result
The custom key function receives all parameters except result and response (which are injected by Clientele).
Conditional Caching
Enable or disable caching based on configuration:
import os
ENABLE_CACHE = os.getenv("ENABLE_CACHE", "true") == "true"
@memoize(ttl=300, enabled=ENABLE_CACHE)
@client.get("/pokemon/{id}")
def get_pokemon(id: int, result: dict) -> dict:
return result
Custom Backend
Swap the default in-memory backend for your own:
from clientele.cache import memoize, MemoryBackend
# Create a backend with smaller cache size
small_cache = MemoryBackend(max_size=50)
@memoize(ttl=300, backend=small_cache)
@client.get("/items/{id}")
def get_item(id: int, result: dict) -> dict:
return result
Alternatively you can set it through the BaseConfig object to prevent constantly setting it through the decorator:
from clientele import api, cache
client = api.APIClient(config=api.BaseConfig(
base_url="https://myapi.com/",
# Set to your own backend here
cache_backend=cache.MemoryBackend
))
@cache.memoize(ttl=300)
@client.get("/items/{id}")
def get_item(id: int, result: dict) -> dict:
return result
Async Support
The @memoize decorator works seamlessly with async functions:
@memoize(ttl=300)
@client.get("/users/{id}")
async def get_user(id: int, result: dict) -> dict:
return result
# Usage
user = await get_user(id=123)
user = await get_user(id=123) # Cached!
Cache Key Generation
Cache keys are automatically generated from:
- HTTP method - Prepended to the key (e.g.,
GET:) - Path template - The API endpoint path
- Function parameters - Sorted alphabetically for consistency
Example cache keys:
# GET /pokemon/{id} with id=25
# Key: "GET:/pokemon/{id}:id=25"
# GET /search with query="python", limit=10
# Key: "GET:/search:limit=10:query=python"
Excluded Parameters
The following parameters are automatically excluded from cache keys because they are injected by Clientele at runtime:
result- The parsed response objectresponse- The raw HTTP responsedata- The request body
Writing a Custom Backend
The MemoryBackend is suitable for single-process applications.
For production systems, you may want Redis, Memcached, or disk-based caching.
Redis Backend example
Implement the CacheBackend protocol:
import redis
from typing import Any, Optional
from clientele import cache
class RedisBackend(cache.CacheBackend):
"""Redis-based cache backend example."""
def __init__(self, redis_url: str):
self.redis = redis.from_url(redis_url)
def get(self, key: str) -> Optional[Any]:
"""Retrieve a value from Redis."""
import pickle
data = self.redis.get(key)
if data is None:
return None
return pickle.loads(data)
def set(self, key: str, value: Any, ttl: Optional[int] = None) -> None:
"""Store a value in Redis with optional TTL."""
import pickle
serialized = pickle.dumps(value)
if ttl is not None:
self.redis.setex(key, ttl, serialized)
else:
self.redis.set(key, serialized)
def delete(self, key: str) -> None:
"""Remove a value from Redis."""
self.redis.delete(key)
def clear(self) -> None:
"""Clear all values (use with caution!)."""
self.redis.flushdb()
def exists(self, key: str) -> bool:
"""Check if a key exists in Redis."""
return bool(self.redis.exists(key))
Using Your Custom Backend
from clientele import cache
# Create and configure your backend
redis_cache = RedisBackend(redis_url="redis://localhost:6379/0")
# Use per-decorator
@cache.memoize(ttl=300, backend=redis_cache)
@client.get("/users/{id}")
def get_user(id: int, result: dict) -> dict:
return result
- API Configuration - General API client configuration
- Async Support - Using async/await with Clientele
- Testing - How to test code that uses caching