Spaces:
Sleeping
Sleeping
Commit ·
38c937d
1
Parent(s): a42771b
feat(deploy): KI-152 — upload_to_hf.py syncs deletions, not just adds
Browse filesAfter upload_folder, walk local tree + list_repo_files, then issue a second
commit that deletes orphan remote files. Prevents ghost brochures lingering
on the Space after KI-126/143/144-style renames. Adds --dry-run + --no-upload
flags. HF_PROTECTED guards .gitattributes / .gitignore / README.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- tools/upload_to_hf.py +139 -11
tools/upload_to_hf.py
CHANGED
|
@@ -6,18 +6,27 @@ huggingface.co/spaces/rohitsar567/InsuranceBot. Post-D-019 the large data
|
|
| 6 |
dataset rohitsar567/insurance-bot-data — the Space's Dockerfile pulls it via
|
| 7 |
huggingface_hub.snapshot_download at image build time.
|
| 8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
Run:
|
| 10 |
-
python tools/upload_to_hf.py
|
|
|
|
| 11 |
"""
|
| 12 |
|
| 13 |
from __future__ import annotations
|
| 14 |
|
|
|
|
|
|
|
| 15 |
import os
|
| 16 |
import sys
|
| 17 |
from pathlib import Path
|
| 18 |
|
| 19 |
from dotenv import load_dotenv
|
| 20 |
-
from huggingface_hub import HfApi
|
| 21 |
|
| 22 |
ROOT = Path(__file__).resolve().parent.parent
|
| 23 |
load_dotenv(ROOT / ".env")
|
|
@@ -62,8 +71,97 @@ IGNORE = [
|
|
| 62 |
"kb/calculations/chunk_*", # Sweep markdown
|
| 63 |
]
|
| 64 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
|
| 66 |
-
def main():
|
| 67 |
token = os.environ.get("HF_TOKEN", "")
|
| 68 |
if not token:
|
| 69 |
print("ERROR: HF_TOKEN not in environment (or .env)")
|
|
@@ -73,16 +171,46 @@ def main():
|
|
| 73 |
print(f"Uploading {ROOT} (code only) -> https://huggingface.co/spaces/{REPO_ID}")
|
| 74 |
print("(data is fetched from the companion dataset at Docker build time)")
|
| 75 |
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
|
| 84 |
print()
|
| 85 |
-
print("✓ Upload complete.")
|
| 86 |
print(f" Space: https://huggingface.co/spaces/{REPO_ID}")
|
| 87 |
print(f" Embed: https://rohitsar567-insurancebot.hf.space")
|
| 88 |
print()
|
|
|
|
| 6 |
dataset rohitsar567/insurance-bot-data — the Space's Dockerfile pulls it via
|
| 7 |
huggingface_hub.snapshot_download at image build time.
|
| 8 |
|
| 9 |
+
KI-152 (2026-05-15): operates in SYNC mode. After `upload_folder` (add/replace),
|
| 10 |
+
walks the local tree, lists the remote repo, and issues a second commit that
|
| 11 |
+
DELETES any remote file that no longer exists locally. Prevents ghost files
|
| 12 |
+
(renamed/deleted brochures) from lingering on the Space and being served by
|
| 13 |
+
the Docker image after a rebuild. See KI-126 / KI-143 / KI-144 / KI-151.
|
| 14 |
+
|
| 15 |
Run:
|
| 16 |
+
python tools/upload_to_hf.py # full upload + sync delete
|
| 17 |
+
python tools/upload_to_hf.py --dry-run # report orphans, no delete commit
|
| 18 |
"""
|
| 19 |
|
| 20 |
from __future__ import annotations
|
| 21 |
|
| 22 |
+
import argparse
|
| 23 |
+
import fnmatch
|
| 24 |
import os
|
| 25 |
import sys
|
| 26 |
from pathlib import Path
|
| 27 |
|
| 28 |
from dotenv import load_dotenv
|
| 29 |
+
from huggingface_hub import CommitOperationDelete, HfApi
|
| 30 |
|
| 31 |
ROOT = Path(__file__).resolve().parent.parent
|
| 32 |
load_dotenv(ROOT / ".env")
|
|
|
|
| 71 |
"kb/calculations/chunk_*", # Sweep markdown
|
| 72 |
]
|
| 73 |
|
| 74 |
+
# HF-managed files that must never be deleted by the sync step. `.gitattributes`
|
| 75 |
+
# is required by HF for LFS pointer files and is autogenerated/maintained by
|
| 76 |
+
# the platform; deleting it breaks future LFS uploads.
|
| 77 |
+
HF_PROTECTED = {".gitattributes", ".gitignore", "README.md"}
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def _matches_ignore(rel_path: str) -> bool:
|
| 81 |
+
"""Return True if `rel_path` matches any IGNORE pattern."""
|
| 82 |
+
# fnmatch doesn't understand `**` the way gitignore does, but the HF hub
|
| 83 |
+
# `ignore_patterns` arg uses the same fnmatch semantics with `**` handled
|
| 84 |
+
# specially. Mirror that: replace `**/` => match any depth.
|
| 85 |
+
for pat in IGNORE:
|
| 86 |
+
if fnmatch.fnmatch(rel_path, pat):
|
| 87 |
+
return True
|
| 88 |
+
# Handle `**/` prefix → match the suffix at any depth.
|
| 89 |
+
if pat.startswith("**/"):
|
| 90 |
+
suffix = pat[3:]
|
| 91 |
+
if fnmatch.fnmatch(rel_path, suffix):
|
| 92 |
+
return True
|
| 93 |
+
if fnmatch.fnmatch(rel_path, f"*/{suffix}"):
|
| 94 |
+
return True
|
| 95 |
+
# Handle `dir/**` → match anything under dir/.
|
| 96 |
+
if pat.endswith("/**"):
|
| 97 |
+
prefix = pat[:-3]
|
| 98 |
+
if rel_path == prefix or rel_path.startswith(prefix + "/"):
|
| 99 |
+
return True
|
| 100 |
+
return False
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def _compute_local_files() -> set[str]:
|
| 104 |
+
"""Walk ROOT (no symlink follow), apply IGNORE, return relative paths."""
|
| 105 |
+
local: set[str] = set()
|
| 106 |
+
for dirpath, dirnames, filenames in os.walk(ROOT, followlinks=False):
|
| 107 |
+
# Prune ignored directories in-place to avoid descending into them.
|
| 108 |
+
rel_dir = os.path.relpath(dirpath, ROOT)
|
| 109 |
+
if rel_dir == ".":
|
| 110 |
+
rel_dir = ""
|
| 111 |
+
kept_dirs = []
|
| 112 |
+
for d in dirnames:
|
| 113 |
+
sub_rel = f"{rel_dir}/{d}" if rel_dir else d
|
| 114 |
+
if _matches_ignore(sub_rel) or _matches_ignore(f"{sub_rel}/"):
|
| 115 |
+
continue
|
| 116 |
+
kept_dirs.append(d)
|
| 117 |
+
dirnames[:] = kept_dirs
|
| 118 |
+
|
| 119 |
+
for fn in filenames:
|
| 120 |
+
sub_rel = f"{rel_dir}/{fn}" if rel_dir else fn
|
| 121 |
+
if _matches_ignore(sub_rel):
|
| 122 |
+
continue
|
| 123 |
+
local.add(sub_rel)
|
| 124 |
+
return local
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def _compute_orphans(api: HfApi) -> list[str]:
|
| 128 |
+
"""Return remote files that should be deleted (no local counterpart)."""
|
| 129 |
+
remote_files = set(api.list_repo_files(repo_id=REPO_ID, repo_type=REPO_TYPE))
|
| 130 |
+
local_files = _compute_local_files()
|
| 131 |
+
|
| 132 |
+
orphans = []
|
| 133 |
+
for rf in sorted(remote_files - local_files):
|
| 134 |
+
if rf in HF_PROTECTED:
|
| 135 |
+
continue
|
| 136 |
+
# An ignored-pattern remote file is still an orphan (uploaded by an
|
| 137 |
+
# older script version) — deleting it is correct hygiene. But never
|
| 138 |
+
# touch HF_PROTECTED.
|
| 139 |
+
orphans.append(rf)
|
| 140 |
+
return orphans
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def _print_orphan_summary(orphans: list[str], action: str) -> None:
|
| 144 |
+
print(f"✓ Orphan {action}: {len(orphans)} paths")
|
| 145 |
+
for o in orphans[:10]:
|
| 146 |
+
print(f" - {o}")
|
| 147 |
+
if len(orphans) > 10:
|
| 148 |
+
print(f" ... and {len(orphans) - 10} more")
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def main() -> int:
|
| 152 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 153 |
+
parser.add_argument(
|
| 154 |
+
"--dry-run",
|
| 155 |
+
action="store_true",
|
| 156 |
+
help="Report orphans without issuing the delete commit (still uploads unless --no-upload).",
|
| 157 |
+
)
|
| 158 |
+
parser.add_argument(
|
| 159 |
+
"--no-upload",
|
| 160 |
+
action="store_true",
|
| 161 |
+
help="Skip the upload_folder step; only compute/report/delete orphans.",
|
| 162 |
+
)
|
| 163 |
+
args = parser.parse_args()
|
| 164 |
|
|
|
|
| 165 |
token = os.environ.get("HF_TOKEN", "")
|
| 166 |
if not token:
|
| 167 |
print("ERROR: HF_TOKEN not in environment (or .env)")
|
|
|
|
| 171 |
print(f"Uploading {ROOT} (code only) -> https://huggingface.co/spaces/{REPO_ID}")
|
| 172 |
print("(data is fetched from the companion dataset at Docker build time)")
|
| 173 |
|
| 174 |
+
if args.dry_run:
|
| 175 |
+
print("[--dry-run] No mutating calls will be made. Orphan list only.")
|
| 176 |
+
|
| 177 |
+
# ---- Step 1: upload_folder (add + replace) -------------------------------
|
| 178 |
+
if args.no_upload or args.dry_run:
|
| 179 |
+
if args.dry_run:
|
| 180 |
+
print("[--dry-run] Skipping upload_folder.")
|
| 181 |
+
else:
|
| 182 |
+
print("[--no-upload] Skipping upload_folder.")
|
| 183 |
+
else:
|
| 184 |
+
api.upload_folder(
|
| 185 |
+
folder_path=str(ROOT),
|
| 186 |
+
repo_id=REPO_ID,
|
| 187 |
+
repo_type=REPO_TYPE,
|
| 188 |
+
ignore_patterns=IGNORE,
|
| 189 |
+
commit_message="Deploy: Stack A (NIM brain + Maverick judge + Sarvam voice). D-019.",
|
| 190 |
+
)
|
| 191 |
+
print("✓ Upload complete (folder upload).")
|
| 192 |
+
|
| 193 |
+
# ---- Step 2: compute + delete orphans -----------------------------------
|
| 194 |
+
orphans = _compute_orphans(api)
|
| 195 |
+
|
| 196 |
+
if not orphans:
|
| 197 |
+
print("✓ Orphan sync: 0 orphans (remote in sync with local).")
|
| 198 |
+
elif args.dry_run:
|
| 199 |
+
_print_orphan_summary(orphans, action="would delete (dry-run)")
|
| 200 |
+
else:
|
| 201 |
+
operations = [CommitOperationDelete(path_in_repo=p) for p in orphans]
|
| 202 |
+
api.create_commit(
|
| 203 |
+
repo_id=REPO_ID,
|
| 204 |
+
repo_type=REPO_TYPE,
|
| 205 |
+
operations=operations,
|
| 206 |
+
commit_message=(
|
| 207 |
+
f"chore(deploy): KI-152 sync — delete {len(orphans)} orphan file(s) "
|
| 208 |
+
"(brochure renames/removals)"
|
| 209 |
+
),
|
| 210 |
+
)
|
| 211 |
+
_print_orphan_summary(orphans, action="delete")
|
| 212 |
|
| 213 |
print()
|
|
|
|
| 214 |
print(f" Space: https://huggingface.co/spaces/{REPO_ID}")
|
| 215 |
print(f" Embed: https://rohitsar567-insurancebot.hf.space")
|
| 216 |
print()
|