#!/usr/bin/env python3 """One-shot end-to-end: shrink DB → (optional B2 bucket) → catalog to B2 or Hub → HF Space secrets/vars → publish + verify. **Requires** ``export HF_SPACE_REPO=owner/slug`` (or pass ``--space-repo``). Reads ``parser`` ``secrets.toml`` for ``[huggingface].token`` (and ``[b2]`` unless overridden). Optional ``--b2-secrets-toml`` or ``KINK_B2_SECRETS_TOML`` supplies ``[b2]`` only (e.g. a new B2 account). **Default bootstrap mode** (``--bootstrap auto``): 1. Build ``data/store_slim_b2.db`` from ``data/store_slim.db`` (unless ``--skip-shrink``). 2. Try **B2**: upload + push ``B2_*`` secrets + ``KINK_B2_OBJECT_KEY`` + ``verify_b2_catalog_http``. 3. If B2 upload fails (e.g. storage cap), **Hub fallback**: remove B2 Space secrets so bootstrap does not stop on B2, upload to Hub dataset + set ``KINK_HF_DATASET_*`` variables, then continue. 4. **Publish** the Space repo and **verify** live (same as ``publish_hf_space.py --verify``). Does not print tokens or application keys. Example:: export HF_SPACE_REPO=ronheichman/kink-discovery python scripts/e2e_deploy_kink_catalog.py # Optional: create bucket first (needs writeBuckets on key or --admin master key env) python scripts/e2e_deploy_kink_catalog.py --create-bucket kink-catalog-myname # B2 only (no Hub fallback) python scripts/e2e_deploy_kink_catalog.py --bootstrap b2 # Hub only (skip B2) python scripts/e2e_deploy_kink_catalog.py --bootstrap hub """ from __future__ import annotations import argparse import os import subprocess import sys import tomllib from pathlib import Path ROOT = Path(__file__).resolve().parent.parent DEFAULT_SECRETS = Path(os.environ.get("KINK_PARSER_SECRETS_TOML", Path.home() / "PycharmProjects/parser/secrets.toml")) def _run(py_args: list[str], env: dict | None = None) -> None: cmd = [sys.executable, str(ROOT / "scripts" / py_args[0])] + py_args[1:] e = {**os.environ, **(env or {})} print(f"e2e_deploy: {' '.join(cmd)}", file=sys.stderr, flush=True) r = subprocess.run(cmd, cwd=str(ROOT), env=e) if r.returncode != 0: raise SystemExit(r.returncode) def _load_hf_token(secrets_path: Path) -> str: with secrets_path.open("rb") as f: data = tomllib.load(f) t = ((data.get("huggingface") or {}).get("token") or "").strip() if not t: raise SystemExit(f"e2e_deploy: missing [huggingface].token in {secrets_path}") return t def _delete_b2_space_config(api, repo_id: str, hf_token: str) -> None: """Remove B2 secrets/vars so Space bootstrap can fall through to Hub.""" from huggingface_hub.utils import HfHubHTTPError for key in ("B2_KEY_ID", "B2_APPLICATION_KEY", "B2_BUCKET", "B2_REGION"): try: api.delete_space_secret(repo_id, key, token=hf_token) print(f"e2e_deploy: removed Space secret {key}", file=sys.stderr) except HfHubHTTPError as exc: if getattr(exc, "response", None) is not None and exc.response.status_code in (400, 404): continue print(f"e2e_deploy: delete_space_secret {key}: {exc}", file=sys.stderr) except Exception as exc: print(f"e2e_deploy: delete_space_secret {key}: {exc}", file=sys.stderr) try: api.delete_space_variable(repo_id, "KINK_B2_OBJECT_KEY", token=hf_token) print("e2e_deploy: removed Space variable KINK_B2_OBJECT_KEY", file=sys.stderr) except Exception as exc: print(f"e2e_deploy: delete_space_variable KINK_B2_OBJECT_KEY: {exc}", file=sys.stderr) def _hub_fallback( secrets_path: Path, space_repo: str, local_db: Path, hub_repo: str, remote_filename: str, hf_token: str, ) -> None: from huggingface_hub import HfApi api = HfApi(token=hf_token) _delete_b2_space_config(api, space_repo, hf_token) api.create_repo(hub_repo, repo_type="dataset", exist_ok=True) print( f"e2e_deploy: Hub fallback — uploading {local_db} → {hub_repo}/{remote_filename}", file=sys.stderr, ) api.upload_file( path_or_fileobj=str(local_db), path_in_repo=remote_filename, repo_id=hub_repo, repo_type="dataset", ) api.add_space_variable( space_repo, "KINK_HF_DATASET_FILENAME", remote_filename, description="Bootstrap SQLite (Hub) from e2e_deploy_kink_catalog", token=hf_token, ) api.add_space_variable( space_repo, "KINK_HF_DATASET_REPO", hub_repo, description="Hub dataset for catalog", token=hf_token, ) print("e2e_deploy: Hub fallback Space variables set.", file=sys.stderr) def _try_b2_chain( secrets_path: Path, space_repo: str, local_db: Path, b2_object_key: str, b2_secrets_toml: Path | None, ) -> bool: """Return True if B2 upload + HF secrets + verify succeeded.""" env = { **os.environ, "HF_SPACE_REPO": space_repo, "KINK_PARSER_SECRETS_TOML": str(secrets_path), } if b2_secrets_toml is not None: env["KINK_B2_SECRETS_TOML"] = str(b2_secrets_toml.resolve()) cmd = [ sys.executable, str(ROOT / "scripts" / "sync_hf_space_b2_catalog.py"), "--secrets-toml", str(secrets_path), "--local-path", str(local_db), "--key", b2_object_key, ] if b2_secrets_toml is not None: cmd.extend(["--b2-secrets-toml", str(b2_secrets_toml.resolve())]) print(f"e2e_deploy: {' '.join(cmd)}", file=sys.stderr, flush=True) r = subprocess.run(cmd, cwd=str(ROOT), env=env) if r.returncode != 0: return False verify_args = [ "verify_b2_catalog_http.py", "--secrets-toml", str(secrets_path), "--key", b2_object_key, ] if b2_secrets_toml is not None: verify_args.extend(["--b2-secrets-toml", str(b2_secrets_toml.resolve())]) _run(verify_args) return True def main() -> int: ap = argparse.ArgumentParser(description=__doc__) ap.add_argument("--secrets-toml", type=Path, default=DEFAULT_SECRETS) ap.add_argument( "--b2-secrets-toml", type=Path, default=None, help="TOML with only [b2] (also set KINK_B2_SECRETS_TOML); parser --secrets-toml still used for HF token", ) ap.add_argument("--space-repo", default=os.environ.get("HF_SPACE_REPO", "").strip(), help="HF Space repo id") ap.add_argument("--source", type=Path, default=ROOT / "data" / "store_slim.db") ap.add_argument("--shrink-dest", type=Path, default=ROOT / "data" / "store_slim_b2.db") ap.add_argument("--skip-shrink", action="store_true") ap.add_argument( "--create-bucket", metavar="NAME", default="", help="If set, run b2_create_bucket.py --name NAME first (uses [b2] or B2_ADMIN_* env with --admin-bucket-create)", ) ap.add_argument( "--admin-bucket-create", action="store_true", help="With --create-bucket, pass --admin to b2_create_bucket (needs B2_ADMIN_KEY_ID / B2_ADMIN_APPLICATION_KEY)", ) ap.add_argument( "--bootstrap", choices=("auto", "b2", "hub"), default="auto", help="auto: try B2 then Hub on failure; b2: B2 only; hub: Hub only", ) ap.add_argument("--b2-object-key", default="kink/catalog/store_slim_bootstrap.db") ap.add_argument("--hub-repo", default="ronheichman/kink-catalog-slim") ap.add_argument("--hub-filename", default="store_slim_bootstrap.db") ap.add_argument("--no-publish", action="store_true", help="Skip publish_hf_space.py (still shrink + catalog)") ap.add_argument( "--verify-min-kinks", type=int, default=90_000, help="HF_VERIFY_MIN_KINKS for verify_hf_stack (default 90000)", ) args = ap.parse_args() space_repo = (args.space_repo or "").strip() if not space_repo: print("e2e_deploy: set --space-repo or HF_SPACE_REPO", file=sys.stderr) return 1 secrets_path = args.secrets_toml.resolve() if not secrets_path.is_file(): print(f"e2e_deploy: secrets not found: {secrets_path}", file=sys.stderr) return 1 if args.b2_secrets_toml is not None and not args.b2_secrets_toml.is_file(): print(f"e2e_deploy: --b2-secrets-toml not found: {args.b2_secrets_toml}", file=sys.stderr) return 1 hf_token = _load_hf_token(secrets_path) os.environ["HF_TOKEN"] = hf_token os.environ["HF_SPACE_REPO"] = space_repo if args.create_bucket: bc = [ "b2_create_bucket.py", "--secrets-toml", str(secrets_path), "--name", args.create_bucket, ] if args.admin_bucket_create: bc.append("--admin") if args.b2_secrets_toml is not None: bc.extend(["--b2-secrets-toml", str(args.b2_secrets_toml.resolve())]) _run(bc) if not args.skip_shrink: src = args.source.resolve() dst = args.shrink_dest.resolve() if not src.is_file(): print(f"e2e_deploy: missing source DB {src}", file=sys.stderr) return 1 _run( [ "shrink_store_for_remote_bootstrap.py", "--source", str(src), "--dest", str(dst), "--force", ] ) local_db = args.shrink_dest.resolve() if not local_db.is_file(): print(f"e2e_deploy: missing catalog file {local_db}", file=sys.stderr) return 1 used_b2 = False if args.bootstrap in ("auto", "b2"): ok = _try_b2_chain(secrets_path, space_repo, local_db, args.b2_object_key, args.b2_secrets_toml) if ok: used_b2 = True print("e2e_deploy: B2 path OK.", file=sys.stderr) elif args.bootstrap == "b2": print("e2e_deploy: B2 path failed and --bootstrap b2 (no Hub fallback).", file=sys.stderr) return 1 else: print("e2e_deploy: B2 failed — Hub fallback.", file=sys.stderr) _hub_fallback( secrets_path, space_repo, local_db, args.hub_repo, args.hub_filename, hf_token, ) else: _hub_fallback( secrets_path, space_repo, local_db, args.hub_repo, args.hub_filename, hf_token, ) if not used_b2: _run( [ "prove_hub_bootstrap_local.py", "--secrets-toml", str(secrets_path), "--repo-id", args.hub_repo, "--filename", args.hub_filename, ] ) if args.no_publish: print("e2e_deploy: --no-publish — done (no Space code push).", file=sys.stderr) return 0 env_verify = { **os.environ, "HF_TOKEN": hf_token, "HF_SPACE_REPO": space_repo, "HF_VERIFY_MIN_KINKS": str(args.verify_min_kinks), } _run(["publish_hf_space.py", "--verify"], env=env_verify) print("e2e_deploy: complete.", file=sys.stderr) return 0 if __name__ == "__main__": raise SystemExit(main())