#!/usr/bin/env python3 """Upload the slim bootstrap SQLite to B2 and push B2 credentials to a Hugging Face Space. Reads ``[b2]`` and ``[huggingface].token`` from parser ``secrets.toml`` (same defaults as other scripts). Uploads ``data/store_slim_b2.db`` by default (output of ``shrink_store_for_remote_bootstrap.py`` — full kink rows, ~280 MiB). Then calls ``HfApi.add_space_secret`` for ``B2_*`` and ``add_space_variable`` for ``KINK_B2_OBJECT_KEY`` so the Space bootstraps via S3 **before** Hub (see ``backend/hf_bootstrap``). Requires: export HF_SPACE_REPO=owner/slug Does not print secret values. After success, rebuild the Space so the container picks up secrets:: python scripts/publish_hf_space.py --verify """ from __future__ import annotations import argparse import os import sys import tomllib from pathlib import Path ROOT = Path(__file__).resolve().parent.parent if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) from backend.b2_toml import load_b2_section, resolve_b2_config_path from backend.repo_dotenv import load_repo_dotenv DEFAULT_SECRETS = Path(os.environ.get("KINK_PARSER_SECRETS_TOML", Path.home() / "PycharmProjects/parser/secrets.toml")) def _load_hf_token(path: Path) -> str: t = (os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN") or "").strip() if t: return t with path.open("rb") as f: data = tomllib.load(f) hf = (data.get("huggingface") or {}).get("token") or "" if not str(hf).strip(): raise SystemExit(f"Missing [huggingface].token in {path}") return str(hf).strip() def main() -> int: load_repo_dotenv() ap = argparse.ArgumentParser(description=__doc__) ap.add_argument( "--secrets-toml", type=Path, default=DEFAULT_SECRETS, help="Parser secrets with [huggingface].token (and [b2] if --b2-secrets-toml not set)", ) ap.add_argument( "--b2-secrets-toml", type=Path, default=None, help="TOML with only [b2] (overrides env KINK_B2_SECRETS_TOML and [b2] from --secrets-toml)", ) ap.add_argument( "--local-path", type=Path, default=ROOT / "data" / "store_slim_b2.db", help="SQLite file to upload (default: shrunk bootstrap DB)", ) ap.add_argument( "--key", default="kink/catalog/store_slim_bootstrap.db", help="S3 object key (Space default KINK_B2_OBJECT_KEY must match)", ) ap.add_argument("--skip-upload", action="store_true", help="Only push HF secrets/variables") args = ap.parse_args() repo_id = (os.environ.get("HF_SPACE_REPO") or "").strip() if not repo_id: print("sync_hf_space_b2_catalog: set HF_SPACE_REPO=owner/slug", file=sys.stderr) return 1 if not args.secrets_toml.is_file(): print(f"sync_hf_space_b2_catalog: secrets not found: {args.secrets_toml}", file=sys.stderr) return 1 b2_path = resolve_b2_config_path(cli_b2=args.b2_secrets_toml, cli_fallback=args.secrets_toml) if not b2_path.is_file(): print(f"sync_hf_space_b2_catalog: B2 secrets TOML not found: {b2_path}", file=sys.stderr) return 1 try: b2 = load_b2_section(b2_path) except SystemExit as e: print(f"sync_hf_space_b2_catalog: {e}", file=sys.stderr) return 1 try: hf_token = _load_hf_token(args.secrets_toml) except SystemExit as e: print(f"sync_hf_space_b2_catalog: {e}", file=sys.stderr) return 1 local = args.local_path.resolve() if not args.skip_upload: if not local.is_file(): print(f"sync_hf_space_b2_catalog: missing {local} — run shrink_store_for_remote_bootstrap.py first", file=sys.stderr) return 1 import boto3 from huggingface_hub import HfApi region = b2["region"] endpoint = f"https://s3.{region}.backblazeb2.com" client = boto3.client( "s3", endpoint_url=endpoint, aws_access_key_id=b2["key_id"], aws_secret_access_key=b2["application_key"], region_name=region, ) env_bucket = (os.environ.get("KINK_B2_CATALOG_BUCKET") or os.environ.get("B2_CATALOG_BUCKET") or "").strip() bucket = env_bucket or b2["bucket_name"] key = args.key.lstrip("/") if env_bucket: print(f"sync_hf_space_b2_catalog: using bucket from env: {bucket}", file=sys.stderr) if not args.skip_upload: print( f"sync_hf_space_b2_catalog: uploading {local} ({local.stat().st_size / 1e6:.1f} MB) → s3://{bucket}/{key}", file=sys.stderr, ) try: client.upload_file(str(local), bucket, key, ExtraArgs={"ContentType": "application/x-sqlite3"}) except Exception as e: err = str(e).lower() if "storage cap" in err or "cap exceeded" in err: print( "sync_hf_space_b2_catalog: B2 **account** Caps & Alerts (storage $/day) hit — " "not fixed by a new object key or new empty bucket on the same account. " "Raise/remove caps in the B2 console, or use another account / KINK_B2_CATALOG_BUCKET on an account with room.", file=sys.stderr, ) raise api = HfApi(token=hf_token) def sec(name: str, value: str, desc: str) -> None: api.add_space_secret(repo_id, name, value, description=desc, token=hf_token) sec("B2_KEY_ID", b2["key_id"], "Backblaze B2 application key id (S3)") sec("B2_APPLICATION_KEY", b2["application_key"], "Backblaze B2 application key secret") sec("B2_BUCKET", bucket, "Backblaze B2 bucket for catalog object") sec("B2_REGION", region, "Backblaze B2 region (e.g. us-east-005)") api.add_space_variable( repo_id, "KINK_B2_OBJECT_KEY", key, description="S3 object key for store SQLite (bootstrap)", token=hf_token, ) print("sync_hf_space_b2_catalog: Space secrets + KINK_B2_OBJECT_KEY updated.", file=sys.stderr) print("sync_hf_space_b2_catalog: Rebuild the Space:", file=sys.stderr) print(f" HF_TOKEN=*** HF_SPACE_REPO={repo_id} python scripts/publish_hf_space.py --verify", file=sys.stderr) return 0 if __name__ == "__main__": raise SystemExit(main())