nutrilens / src /cache.py
vicarioush's picture
Initial NutriLens submission
8e54894
Raw
History Blame Contribute Delete
4.29 kB
"""
SQLite-based caching layer for nutrition and literature API responses.
Works both locally and on HF Spaces (file-based, no server needed).
"""
import sqlite3
import json
import hashlib
import time
import os
from pathlib import Path
from typing import Optional
# Default cache location: project root or HF Space persistent storage
CACHE_DIR = os.environ.get("CACHE_DIR", str(Path(__file__).parent.parent))
CACHE_DB = os.path.join(CACHE_DIR, "cache.db")
def _get_connection() -> sqlite3.Connection:
"""Get a SQLite connection with WAL mode for concurrent reads."""
conn = sqlite3.connect(CACHE_DB, timeout=10)
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA synchronous=NORMAL")
return conn
def init_cache():
"""Create cache tables if they don't exist."""
conn = _get_connection()
try:
conn.executescript("""
CREATE TABLE IF NOT EXISTS nutrition_cache (
query_key TEXT PRIMARY KEY,
response_json TEXT NOT NULL,
created_at REAL NOT NULL,
ttl_days INTEGER DEFAULT 30
);
CREATE TABLE IF NOT EXISTS literature_cache (
query_key TEXT PRIMARY KEY,
response_json TEXT NOT NULL,
created_at REAL NOT NULL,
ttl_days INTEGER DEFAULT 7
);
""")
conn.commit()
finally:
conn.close()
def _make_key(text: str) -> str:
"""Create a normalized cache key from query text."""
normalized = text.strip().lower()
return hashlib.sha256(normalized.encode()).hexdigest()[:32]
def get_cached(table: str, query: str) -> Optional[dict]:
"""
Retrieve a cached response if it exists and hasn't expired.
Args:
table: 'nutrition_cache' or 'literature_cache'
query: the search query string
Returns:
Parsed JSON dict if cache hit and not expired, None otherwise.
"""
init_cache()
key = _make_key(query)
conn = _get_connection()
try:
row = conn.execute(
f"SELECT response_json, created_at, ttl_days FROM {table} WHERE query_key = ?",
(key,),
).fetchone()
if row is None:
return None
response_json, created_at, ttl_days = row
age_days = (time.time() - created_at) / 86400
if age_days > ttl_days:
# Expired, clean up
conn.execute(f"DELETE FROM {table} WHERE query_key = ?", (key,))
conn.commit()
return None
return json.loads(response_json)
finally:
conn.close()
def set_cached(table: str, query: str, data: dict, ttl_days: int = None):
"""
Store a response in the cache.
Args:
table: 'nutrition_cache' or 'literature_cache'
query: the search query string
data: response dict to cache
ttl_days: override default TTL
"""
init_cache()
key = _make_key(query)
ttl = ttl_days or (30 if table == "nutrition_cache" else 7)
conn = _get_connection()
try:
conn.execute(
f"""INSERT OR REPLACE INTO {table}
(query_key, response_json, created_at, ttl_days)
VALUES (?, ?, ?, ?)""",
(key, json.dumps(data), time.time(), ttl),
)
conn.commit()
finally:
conn.close()
def clear_expired():
"""Remove all expired entries from all cache tables."""
init_cache()
conn = _get_connection()
now = time.time()
try:
for table in ["nutrition_cache", "literature_cache"]:
conn.execute(
f"DELETE FROM {table} WHERE (? - created_at) / 86400 > ttl_days",
(now,),
)
conn.commit()
finally:
conn.close()
def cache_stats() -> dict:
"""Return count and size info for monitoring."""
init_cache()
conn = _get_connection()
try:
stats = {}
for table in ["nutrition_cache", "literature_cache"]:
count = conn.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0]
stats[table] = count
db_size_mb = os.path.getsize(CACHE_DB) / (1024 * 1024)
stats["db_size_mb"] = round(db_size_mb, 2)
return stats
finally:
conn.close()