""" Free persistent storage for the SQLite DB via a private Hugging Face Dataset. Hugging Face free Spaces wipe local files on rebuild, so the sentiment scorecard, meta-model training data and magnet track record can't accumulate. This backs the whole SQLite file up to a private HF Dataset periodically and restores it on startup — zero changes to the app's SQL, works on the free tier. Setup (all free): 1. Create a PRIVATE dataset at https://huggingface.co/new-dataset e.g. areebithink/crypto-terminal-db 2. Set these env vars / HF secrets: HF_DB_DATASET=areebithink/crypto-terminal-db HF_TOKEN=hf_... (a WRITE token) If unset, this is a no-op and the app runs exactly as before (ephemeral). """ import os import time import shutil import tempfile import logging logger = logging.getLogger(__name__) _DB_FILENAME = "narrative_analysis.db" _EMAILS_FILENAME = "emails.db" _last_backup = 0.0 _last_email_backup = 0.0 def _cfg(): return os.getenv("HF_DB_DATASET"), os.getenv("HF_TOKEN") def persistence_configured() -> bool: ds, tok = _cfg() return bool(ds and tok) def restore_db(local_path: str) -> bool: """Download the last backup into local_path if we don't already have data.""" ds, tok = _cfg() if not (ds and tok): return False # Don't clobber a local DB that already has real data if os.path.exists(local_path) and os.path.getsize(local_path) > 8192: return False try: from huggingface_hub import hf_hub_download p = hf_hub_download(repo_id=ds, filename=_DB_FILENAME, repo_type="dataset", token=tok) shutil.copy(p, local_path) logger.info(f"[Persist] restored DB from {ds}") return True except Exception as exc: logger.info(f"[Persist] no prior backup to restore ({exc})") return False def backup_db(db, min_interval: int = 1800) -> None: """Snapshot + upload the DB, throttled to at most once per `min_interval` s.""" global _last_backup ds, tok = _cfg() if not (ds and tok): return now = time.time() if now - _last_backup < min_interval: return try: tmp = os.path.join(tempfile.gettempdir(), "nna_backup.db") if not db.snapshot_to(tmp): return from huggingface_hub import upload_file upload_file(path_or_fileobj=tmp, path_in_repo=_DB_FILENAME, repo_id=ds, repo_type="dataset", token=tok, commit_message="db backup") _last_backup = now logger.info(f"[Persist] backed up DB to {ds}") except Exception as exc: logger.warning(f"[Persist] backup failed: {exc}") def restore_emails(local_path: str) -> bool: """Restore the separate emails DB on startup (only if we don't have one).""" ds, tok = _cfg() if not (ds and tok): return False if os.path.exists(local_path) and os.path.getsize(local_path) > 8192: return False try: from huggingface_hub import hf_hub_download p = hf_hub_download(repo_id=ds, filename=_EMAILS_FILENAME, repo_type="dataset", token=tok) shutil.copy(p, local_path) logger.info(f"[Persist] restored emails DB from {ds}") return True except Exception as exc: logger.info(f"[Persist] no prior emails backup to restore ({exc})") return False def backup_emails(edb, min_interval: int = 0) -> None: """Snapshot + upload the small emails DB. Defaults to no throttle so every signup is saved immediately (the file is tiny).""" global _last_email_backup ds, tok = _cfg() if not (ds and tok): return now = time.time() if now - _last_email_backup < min_interval: return try: tmp = os.path.join(tempfile.gettempdir(), "emails_backup.db") if not edb.snapshot_to(tmp): return from huggingface_hub import upload_file upload_file(path_or_fileobj=tmp, path_in_repo=_EMAILS_FILENAME, repo_id=ds, repo_type="dataset", token=tok, commit_message="emails backup") _last_email_backup = now logger.info(f"[Persist] backed up emails DB to {ds}") except Exception as exc: logger.warning(f"[Persist] emails backup failed: {exc}")