rohitsar567 Claude Opus 4.7 (1M context) commited on
Commit
38c937d
·
1 Parent(s): a42771b

feat(deploy): KI-152 — upload_to_hf.py syncs deletions, not just adds

Browse files

After 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>

Files changed (1) hide show
  1. 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
- api.upload_folder(
77
- folder_path=str(ROOT),
78
- repo_id=REPO_ID,
79
- repo_type=REPO_TYPE,
80
- ignore_patterns=IGNORE,
81
- commit_message="Deploy: Stack A (NIM brain + Maverick judge + Sarvam voice). D-019.",
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()