RandomZ / app /cache /style_cache.py
StormShadow308's picture
- Updated `.env.example` to include new settings for bulk upload limits and batch processing.
3c31a2a
Raw
History Blame Contribute Delete
3.45 kB
"""Per-tenant writing style profile cache.
Style profiles are persisted as JSON files under ``settings.cache_dir / styles/``
so they survive server restarts. Each file is named ``<tenant_id>.json``.
Cached profiles expire after ``_STYLE_CACHE_TTL_DAYS`` days so that newly
uploaded documents can shift the detected style on the next generation call.
"""
import json
import logging
import time
from pathlib import Path
from app.config import settings
from app.models.schemas import WritingStyleProfile
logger = logging.getLogger(__name__)
# Style profiles are re-analysed after this many days of inactivity.
# Set to 0 to disable TTL (cache lives forever until explicitly invalidated).
_STYLE_CACHE_TTL_DAYS: int = 7
_STYLE_CACHE_TTL_SECS: float = _STYLE_CACHE_TTL_DAYS * 86_400
def _style_dir() -> Path:
d = Path(settings.cache_dir) / "styles"
d.mkdir(parents=True, exist_ok=True)
return d
def _path(tenant_id: str) -> Path:
safe = "".join(c if c.isalnum() or c in "-_" else "_" for c in tenant_id)
return _style_dir() / f"{safe}.json"
def get(tenant_id: str) -> WritingStyleProfile | None:
"""Return a cached :class:`WritingStyleProfile` for ``tenant_id``, or ``None``.
Returns ``None`` (forcing a fresh analysis) when:
* No cached file exists.
* The cached file is older than ``_STYLE_CACHE_TTL_DAYS``.
* The cached file is corrupt / unreadable.
Args:
tenant_id: The tenant whose style profile to look up.
Returns:
:class:`WritingStyleProfile` if cached and fresh, otherwise ``None``.
"""
p = _path(tenant_id)
if not p.exists():
return None
# TTL check — expire stale profiles so new uploads can shift the style.
if _STYLE_CACHE_TTL_SECS > 0:
age = time.time() - p.stat().st_mtime
if age > _STYLE_CACHE_TTL_SECS:
logger.debug(
"Style cache expired (%.0f days old) for tenant=%s — will re-analyse",
age / 86_400,
tenant_id,
)
p.unlink(missing_ok=True)
return None
try:
data = json.loads(p.read_text(encoding="utf-8"))
return WritingStyleProfile(**data)
except Exception as exc:
logger.warning("Could not read style cache for %s: %s", tenant_id, exc)
return None
def set(tenant_id: str, profile: WritingStyleProfile) -> None:
"""Persist ``profile`` for ``tenant_id`` atomically.
Writes to a temp file then renames so a crash mid-write never corrupts
the stored profile.
Args:
tenant_id: The tenant whose profile to store.
profile: :class:`WritingStyleProfile` to serialise and save.
"""
p = _path(tenant_id)
tmp = p.with_suffix(".tmp")
try:
tmp.write_text(profile.model_dump_json(indent=2), encoding="utf-8")
tmp.replace(p) # atomic rename
logger.debug("Style profile cached for tenant=%s", tenant_id)
except Exception as exc:
logger.warning("Could not write style cache for %s: %s", tenant_id, exc)
tmp.unlink(missing_ok=True)
def invalidate(tenant_id: str) -> None:
"""Remove the cached profile for ``tenant_id`` (force re-analysis on next call).
Args:
tenant_id: The tenant whose cache entry to delete.
"""
p = _path(tenant_id)
if p.exists():
p.unlink(missing_ok=True)
logger.debug("Style cache invalidated for tenant=%s", tenant_id)