"""Storage abstraction for book PDFs. Why this exists: stage 2 acquisition writes PDFs and stages 3+ read them. On a single laptop they all live under `data/raw/`; on cloud (Streamlit Community Cloud has an ephemeral disk) we want the same code reading from Cloudflare R2 without per-call branching. Public API: get_storage() -> Storage Storage.ensure_local(book_id) -> Path # download to local cache if remote Storage.put_pdf(book_id, src) -> str # returns the storage URI Storage.exists(book_id) -> bool Storage.delete(book_id) -> None Storage.list_book_ids() -> list[str] Backends are picked by `backends.storage` in config.yaml: - 'local' (default): files under cfg.paths.raw_dir. - 'r2': Cloudflare R2 bucket via boto3 (S3-compatible). Needs R2_* env vars from .env / st.secrets. Auto-promotion: when RUNTIME=cloud, `local` is silently promoted to `r2` so the cloud deploy never has to remember to flip config.yaml. Same pattern as the inventory_db backend. """ from __future__ import annotations from .base import Storage from .factory import get_storage __all__ = ["Storage", "get_storage"]