Spaces:
Running
Running
File size: 8,432 Bytes
d6c7413 113b714 d6c7413 33ea2ed d6c7413 33ea2ed d6c7413 33ea2ed d6c7413 33ea2ed d6c7413 33ea2ed d6c7413 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 | """Keep the files that exist in BOTH projects byte-identical.
python huggingface_space/sync_shared.py # report drift, change nothing
python huggingface_space/sync_shared.py --push # repo root -> huggingface_space
python huggingface_space/sync_shared.py --pull # huggingface_space -> repo root
Run it before every upload. It exits non-zero when the folder is not deployable, so it
works as a pre-upload gate rather than something you have to remember to read.
The Space and the live lab are deliberately separate deployments: the Space installs from
its own minimal requirements.txt and must not import the lab, and nothing done to make a
demo look good may reach back and perturb a frozen forward test. So the overlap is held as
COPIES, not imports or symlinks.
Copies drift. That is the entire cost of this arrangement, and the only defence is a tool
that notices. Run this before uploading to Hugging Face.
Direction is never inferred. `--push` and `--pull` are explicit because guessing which
side is authoritative is how one gets silently overwritten.
"""
from __future__ import annotations
import argparse
import filecmp
import hashlib
import shutil
import sys
from pathlib import Path
SPACE = Path(__file__).resolve().parent
ROOT = SPACE.parent
# (path relative to repo root, path relative to huggingface_space, git-tracked in Space?)
#
# trained_models is GIT_IGNORED on the Space side: the same 19 MB of weights is already
# tracked at the repo root, and git keeps blobs forever, so committing them twice would
# double them in every clone permanently. The Space copy is a build artifact this script
# regenerates -- which only works if something refuses to call the folder deployable when
# it is missing. That is what UPLOAD_REQUIRED is for.
SHARED = [
("local_data", "local_data", True),
("trained_models", "trained_models", False),
("trade_analysis/live_lab/indicators.py", "trade_analysis/lab_indicators.py", True),
# The US equity calendar. The Space needs it to say WHICH session a signal is from --
# without it a Labor Day visitor sees Friday's tape presented as today's. Held as a
# copy for the same reason as the rest: the Space must build with nothing from the
# parent repo. It is the file most likely to drift, because holidays get appended to
# the root copy each year and nothing would otherwise notice the Space's copy aging.
("trade_analysis/bulk_download/trading_days.py", "trade_analysis/trading_days.py", True),
]
# Present on disk before uploading, whether or not this repo versions them.
UPLOAD_REQUIRED = ["trained_models", "local_data", "trade_analysis/lab_indicators.py"]
# Files the ROOT keeps but the Space deliberately does NOT ship, so their absence is a
# decision rather than drift. All five fail `torch.load(weights_only=True)` -- they are full
# pickles, not plain tensors -- so tft_model.py could never load them anyway, and they are
# exactly the files Hugging Face flags without a "Safe" badge. Shipping 8 MB of
# unloadable pickles as the only security warnings on a public repo is strictly worse than
# not shipping them.
#
# The root keeps them because trade_analysis/models/tft_backtest.py reads
# tft_AMZN_e200_.pth directly. Do not "fix" this by deleting them there.
SPACE_EXCLUDES = {
"trained_models": {
"tft_AMZN_e200_.pth", "tft_MSFT_e200_.pth", "tft_SPY_e200_.pth",
"tft_TSLA_e200_.pth", "tft_model.pth",
}
}
# lab_indicators.py carries a provenance banner the lab's own copy must not have, so it is
# compared on the CODE below the banner rather than byte-for-byte.
BANNER_EXEMPT = {"trade_analysis/lab_indicators.py"}
def _digest(p: Path) -> str:
return hashlib.sha256(p.read_bytes()).hexdigest()[:12]
def _body(p: Path) -> str:
"""Everything after the module docstring, so a differing banner does not read as drift."""
txt = p.read_text(encoding="utf-8")
parts = txt.split('"""')
return '"""'.join(parts[2:]) if len(parts) >= 3 else txt
def compare(root_rel: str, space_rel: str) -> tuple[str, list[str]]:
a, b = ROOT / root_rel, SPACE / space_rel
if not a.exists():
return "MISSING_ROOT", []
if not b.exists():
return "MISSING_SPACE", []
if a.is_dir():
excl = SPACE_EXCLUDES.get(space_rel, set())
cmp = filecmp.dircmp(str(a), str(b))
# left_only minus the deliberate exclusions: a file the root has and the Space
# intentionally omits is not drift.
diffs = (list(cmp.diff_files)
+ [f for f in cmp.left_only if f not in excl]
+ list(cmp.right_only))
# dircmp is shallow by default: same size + mtime counts as equal. Re-check the
# ones it passed by content, because a same-size edit is exactly the drift that
# matters and exactly the one it would miss.
for name in cmp.common_files:
if name in diffs:
continue
if _digest(a / name) != _digest(b / name):
diffs.append(name)
return ("OK" if not diffs else "DRIFT"), sorted(diffs)
if space_rel in BANNER_EXEMPT:
return ("OK" if _body(a) == _body(b) else "DRIFT"), []
return ("OK" if _digest(a) == _digest(b) else "DRIFT"), []
def copy(root_rel: str, space_rel: str, push: bool) -> None:
a, b = ROOT / root_rel, SPACE / space_rel
src, dst = (a, b) if push else (b, a)
if space_rel in BANNER_EXEMPT:
print(f" SKIP {space_rel} (banner differs by design; re-copy by hand)")
return
if src.is_dir():
excl = SPACE_EXCLUDES.get(space_rel, set()) if push else set()
dst.mkdir(parents=True, exist_ok=True)
for f in src.iterdir():
if f.is_file() and f.name not in excl:
shutil.copy2(f, dst / f.name)
else:
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src, dst)
print(f" copied {src.relative_to(ROOT)} -> {dst.relative_to(ROOT)}")
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
g = ap.add_mutually_exclusive_group()
g.add_argument("--push", action="store_true", help="repo root -> huggingface_space")
g.add_argument("--pull", action="store_true", help="huggingface_space -> repo root")
args = ap.parse_args()
mode = "PUSH" if args.push else "PULL" if args.pull else "CHECK"
print("=" * 82)
print(f"SHARED-FILE SYNC [{mode}] root={ROOT}")
print("=" * 82)
drift = 0
for root_rel, space_rel, tracked in SHARED:
status, files = compare(root_rel, space_rel)
label = root_rel if root_rel == space_rel else f"{root_rel} <-> {space_rel}"
if not tracked:
label += " [git-ignored here; regenerated, not committed]"
print(f" [{status:<13}] {label}")
for f in files[:10]:
print(f" - {f}")
if len(files) > 10:
print(f" ... and {len(files) - 10} more")
if status != "OK":
drift += 1
if args.push or args.pull:
copy(root_rel, space_rel, args.push)
# Deployability is a separate question from drift. A fresh clone has no
# trained_models/ on the Space side at all -- correctly, it is git-ignored -- and
# that is not "drift", it is "not built yet". Uploading in that state would ship a
# Space whose TFT silently degrades to _default_prediction() for every symbol, which
# is precisely the class of quiet fallback this project already got burned by once.
print("\n UPLOAD READINESS")
missing = [p for p in UPLOAD_REQUIRED if not (SPACE / p).exists()]
for p in UPLOAD_REQUIRED:
here = SPACE / p
n = len(list(here.iterdir())) if here.is_dir() else (1 if here.exists() else 0)
print(f" [{'OK ' if here.exists() else 'MISSING'}] {p}"
+ (f" ({n} files)" if here.is_dir() else ""))
if missing:
print(" -> NOT deployable. Run: python huggingface_space/sync_shared.py --push")
if mode == "CHECK":
print(f"\n {drift} of {len(SHARED)} shared paths differ."
+ (" Run with --push or --pull." if drift else " No drift."))
else:
print(f"\n {mode} complete; re-run without a flag to confirm.")
return 1 if ((mode == "CHECK" and drift) or missing) else 0
if __name__ == "__main__":
sys.exit(main())
|