---
name: cache_decorator
kind: function
lang: py
domain: core
version: "1.0.0"
purity: impure
signature: "def cache_decorator(store: Any, ttl: float = 0, key_fn: callable | None = None)"
description: "Decorator que cachea el resultado de una funcion en cualquier store persistente compatible (CacheStore o FileCache). La key se genera hasheando (func.__name__, args, sorted(kwargs)) con SHA-256. Soporta funciones sincronas y asincronas."
tags: [cache, decorator, memoize, persistence, async, functional, pendiente-usar]
uses_functions: ["cache_to_sqlite_py_infra", "cache_to_file_py_infra"]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: ["asyncio", "functools", "hashlib", "json"]
params:
  - name: store
    desc: "store persistente compatible (CacheStore o FileCache) que implementa get/set"
  - name: ttl
    desc: "tiempo de vida del cache en segundos (0 significa infinito)"
  - name: key_fn
    desc: "funcion personalizada para generar clave de cache, recibe (func_name, args, kwargs)"
output: "decorator que cachea resultados de la funcion decorada"
tested: true
tests:
  - "Funcion llamada una vez, segunda vez desde cache"
  - "TTL expirado → llama de nuevo"
  - "key_fn custom"
  - "Argumentos distintos → keys distintas"
  - "Funciona con async"
test_file_path: "python/functions/core/cache_decorator_test.py"
file_path: "python/functions/core/cache_decorator.py"
---

## Ejemplo

```python
from infra.cache_to_sqlite import cache_to_sqlite
from core.cache_decorator import cache_decorator

store = cache_to_sqlite("cache.db", namespace="llm")

@cache_decorator(store, ttl=3600)
def call_llm(prompt: str) -> str:
    # llamada costosa a LLM
    return client.complete(prompt)

result = call_llm("explain X")  # primera vez: llama LLM
result = call_llm("explain X")  # segunda vez: desde cache

# Con key_fn custom
@cache_decorator(store, ttl=600, key_fn=lambda fn, args, kw: args[0])
def fetch_user(user_id: str) -> dict:
    return api.get_user(user_id)

# Con async
@cache_decorator(store, ttl=3600)
async def async_call(prompt: str) -> str:
    return await async_client.complete(prompt)
```

## Notas

El store debe implementar `get(key: str) -> Any | None` y `set(key: str, value: Any, ttl: float) -> None`. Detecta automaticamente funciones asincronas con `asyncio.iscoroutinefunction`. La key por defecto usa `json.dumps(..., default=str)` para serializar argumentos no serializables. Si `store.get()` retorna `None`, siempre se ejecuta la funcion (no distingue entre "no en cache" y "valor None almacenado"); para valores que pueden ser None usar `get_or_set` directamente.
