epfo-circulars / scripts /fetch_runtime_assets.py
gauravmeena0708's picture
Upload folder using huggingface_hub
c724b78 verified
Raw History Blame Contribute Delete
4.98 kB
"""Fetch immutable, hash-checked runtime data without growing Space Git history.
Record after committing assets: python scripts/fetch_runtime_assets.py --record-commit SHA
Docker build: python scripts/fetch_runtime_assets.py --output /assets/data
"""
import argparse
from concurrent.futures import ThreadPoolExecutor
import hashlib
import json
from pathlib import Path, PurePosixPath
import re
import tempfile
import time
import urllib.request
REPO = 'gauravmeena0708/pf-circulars'
MANIFEST = Path('deploy/runtime-assets.json')
def digest(path):
value = hashlib.sha256()
with Path(path).open('rb') as stream:
for block in iter(lambda: stream.read(1024 * 1024), b''):
value.update(block)
return value.hexdigest()
def validate(manifest):
if manifest['repo'] != REPO or not re.fullmatch('[a-f0-9]{40}', manifest['revision']):
raise ValueError('Runtime assets require the configured repository and an immutable commit SHA')
seen = set()
for entry in manifest['files']:
path = PurePosixPath(entry['path'])
if path.is_absolute() or '..' in path.parts or '\\' in entry['path'] or str(path) in seen:
raise ValueError('Invalid or duplicate runtime asset path')
# Only the search index is a build-time asset. Original policy PDFs are fetched
# on demand from their public URLs (see policy_pdfs.py) and are not stored in Git.
if not entry['path'].startswith('vector_store/data_index/'):
raise ValueError('Unexpected runtime asset path')
if not re.fullmatch('[a-f0-9]{64}', entry['sha256']) or not 0 < entry['size'] <= 500*1024*1024:
raise ValueError('Invalid runtime asset hash or size')
seen.add(str(path))
def fetch(entry, revision, output):
target = Path(output) / entry['path']
if target.is_file() and target.stat().st_size == entry['size'] and digest(target) == entry['sha256']:
return
target.parent.mkdir(parents=True, exist_ok=True)
url = f'https://media.githubusercontent.com/media/{REPO}/{revision}/{entry["path"]}'
for attempt in range(3):
temporary = None
try:
checksum = hashlib.sha256()
size = 0
request = urllib.request.Request(url, headers={'User-Agent': 'EPFO-Workspace-Build', 'Accept-Encoding': 'identity'})
with urllib.request.urlopen(request, timeout=90) as response, tempfile.NamedTemporaryFile(dir=target.parent, delete=False) as out:
temporary = Path(out.name)
for block in iter(lambda: response.read(1024*1024), b''):
size += len(block)
if size > entry['size']:
raise ValueError('Asset exceeds its recorded size')
checksum.update(block)
out.write(block)
if size != entry['size'] or checksum.hexdigest() != entry['sha256']:
raise ValueError('Runtime asset size or SHA-256 mismatch')
temporary.chmod(0o644)
temporary.replace(target)
print('Verified ' + entry['path'], flush=True)
return
except Exception:
if temporary and temporary.exists():
temporary.unlink()
if attempt == 2:
raise
time.sleep(2 * (attempt+1))
def record(revision, root=Path('.'), path=MANIFEST, preserve_revision=False):
files = sorted(root/'vector_store/data_index'/name for name in
('faiss_index.index', 'faiss_index.passages.db', 'faiss_index.bm25.json.gz'))
manifest = dict(repo=REPO, revision=revision, files=[dict(path=p.relative_to(root).as_posix(), size=p.stat().st_size, sha256=digest(p)) for p in files])
if preserve_revision and path.exists():
previous = json.loads(path.read_text())
validate(previous)
if previous['files'] == manifest['files']:
manifest['revision'] = previous['revision']
validate(manifest)
path.write_text(json.dumps(manifest, indent=2)+'\n')
return manifest
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--manifest', type=Path, default=MANIFEST)
parser.add_argument('--output', type=Path, default=Path('.'))
mode = parser.add_mutually_exclusive_group()
mode.add_argument('--record-commit')
mode.add_argument('--refresh-commit', help='CI: keep the pinned revision when asset bytes have not changed')
args = parser.parse_args()
if args.record_commit or args.refresh_commit:
record(args.record_commit or args.refresh_commit, path=args.manifest, preserve_revision=bool(args.refresh_commit))
return
manifest = json.loads(args.manifest.read_text())
validate(manifest)
with ThreadPoolExecutor(max_workers=4) as pool:
list(pool.map(lambda entry: fetch(entry, manifest['revision'], args.output), manifest['files']))
if __name__ == '__main__':
main()