Spaces:
Running
Drop XLSX mirror layer to satisfy HuggingFace binary-file policy
Browse filesThe previous commit was rejected by HuggingFace ("push was rejected because
it contains binary files. Please use Xet to store binary files. Offending
files: app/data/aemo_gen_info.xlsx") so the sync_to_hf workflow failed
and the Space stayed on the old code.
The mirror layer (live -> raw.githubusercontent.com XLSX -> snapshot) was
redundant: the daily CI commits the parsed JSON snapshot in the same step
as the XLSX, so they always carried identical freshness. Collapsing to a
two-tier fallback (live -> snapshot) keeps the user-visible behaviour
identical while removing the binary file that HF rejects.
- gen_info_fetcher.py: remove XLSX_MIRROR_URL and the mirror branch in
fetch_bess_list. BessListResult.source is now "live" | "snapshot".
- scripts/refresh_bess_list.py: stop writing app/data/aemo_gen_info.xlsx.
Only bess_list.json and bess_list_meta.json are produced.
- .github/workflows/refresh_bess_list.yml: only watch bess_list.json for
changes.
- app/data/aemo_gen_info.xlsx: removed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@@ -31,14 +31,15 @@ jobs:
|
|
| 31 |
- name: Commit if changed
|
| 32 |
run: |
|
| 33 |
set -e
|
| 34 |
-
# Only commit if the parsed JSON
|
| 35 |
-
#
|
| 36 |
-
|
| 37 |
-
|
|
|
|
| 38 |
exit 0
|
| 39 |
fi
|
| 40 |
git config user.name "github-actions[bot]"
|
| 41 |
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
| 42 |
-
git add app/data/bess_list.json app/data/
|
| 43 |
git commit -m "Refresh BESS list snapshot from AEMO XLSX"
|
| 44 |
git push origin master
|
|
|
|
| 31 |
- name: Commit if changed
|
| 32 |
run: |
|
| 33 |
set -e
|
| 34 |
+
# Only commit if the parsed JSON changed. The meta file changes every
|
| 35 |
+
# run (timestamp); committing it alone would churn master without
|
| 36 |
+
# delivering new data.
|
| 37 |
+
if git diff --quiet -- app/data/bess_list.json; then
|
| 38 |
+
echo "No change in bess_list.json; skipping commit."
|
| 39 |
exit 0
|
| 40 |
fi
|
| 41 |
git config user.name "github-actions[bot]"
|
| 42 |
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
| 43 |
+
git add app/data/bess_list.json app/data/bess_list_meta.json
|
| 44 |
git commit -m "Refresh BESS list snapshot from AEMO XLSX"
|
| 45 |
git push origin master
|
|
@@ -5,10 +5,10 @@ Sources tried in order on every cache miss:
|
|
| 5 |
|
| 6 |
1. live β direct download from www.aemo.com.au (subject to WAF blocks
|
| 7 |
from datacentre IPs; see _BROWSER_HEADERS).
|
| 8 |
-
2.
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
|
| 13 |
The first successful step is cached in memory for 24 hours. The result
|
| 14 |
carries a `source` field and any user-facing `warnings` so the API can
|
|
@@ -32,14 +32,6 @@ XLSX_URL = (
|
|
| 32 |
"?rev=1f6bccf827284f9fb6d6f3ae56ed3fe9&sc_lang=en"
|
| 33 |
)
|
| 34 |
|
| 35 |
-
# raw.githubusercontent.com copy of the XLSX, refreshed daily by
|
| 36 |
-
# .github/workflows/refresh_bess_list.yml. Used as the second-choice source
|
| 37 |
-
# when the live AEMO fetch fails (e.g. WAF 403 from the HuggingFace egress IP).
|
| 38 |
-
XLSX_MIRROR_URL = (
|
| 39 |
-
"https://raw.githubusercontent.com/pourmousavi/BESS-SCADA-Data"
|
| 40 |
-
"/master/app/data/aemo_gen_info.xlsx"
|
| 41 |
-
)
|
| 42 |
-
|
| 43 |
# AEMO's Azure Front Door WAF on www.aemo.com.au returns 403 to requests that
|
| 44 |
# look like default Python clients. Sending a full browser-style header set
|
| 45 |
# bypasses the simpler bot-detection rules. (Doesn't help against pure
|
|
@@ -94,8 +86,8 @@ CACHE_TTL = timedelta(hours=24)
|
|
| 94 |
class BessListResult:
|
| 95 |
"""Parsed BESS list plus provenance for the API response."""
|
| 96 |
states: dict[str, list[dict]]
|
| 97 |
-
source: str # "live" | "
|
| 98 |
-
fetched_at: datetime # for the live
|
| 99 |
warnings: list[str] = field(default_factory=list)
|
| 100 |
|
| 101 |
|
|
@@ -251,10 +243,14 @@ def _load_snapshot() -> BessListResult:
|
|
| 251 |
|
| 252 |
async def fetch_bess_list() -> BessListResult:
|
| 253 |
"""
|
| 254 |
-
Return the BESS list, trying live AEMO β
|
|
|
|
|
|
|
|
|
|
|
|
|
| 255 |
|
| 256 |
-
Uses a 24-hour in-memory cache
|
| 257 |
-
|
| 258 |
"""
|
| 259 |
global _cache
|
| 260 |
|
|
@@ -268,43 +264,25 @@ async def fetch_bess_list() -> BessListResult:
|
|
| 268 |
now - cached_at, cached.source)
|
| 269 |
return cached
|
| 270 |
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
states = await _download_and_parse(XLSX_URL, client)
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
|
| 282 |
-
|
| 283 |
-
try:
|
| 284 |
-
logger.info("Downloading AEMO XLSX (mirror): %s", XLSX_MIRROR_URL)
|
| 285 |
-
states = await _download_and_parse(XLSX_MIRROR_URL, client)
|
| 286 |
-
result = BessListResult(
|
| 287 |
-
states=states,
|
| 288 |
-
source="mirror",
|
| 289 |
-
fetched_at=now,
|
| 290 |
-
warnings=[
|
| 291 |
-
"Live AEMO BESS list is unavailable; showing a copy mirrored "
|
| 292 |
-
"from GitHub (updated daily)."
|
| 293 |
-
],
|
| 294 |
-
)
|
| 295 |
-
_cache = (result, now)
|
| 296 |
-
return result
|
| 297 |
-
except Exception as exc:
|
| 298 |
-
logger.warning("Mirror XLSX fetch failed: %s", exc)
|
| 299 |
-
|
| 300 |
-
# 3. Bundled snapshot β last resort. Always serve something rather than 5xx.
|
| 301 |
try:
|
| 302 |
snapshot = _load_snapshot()
|
| 303 |
age = now - snapshot.fetched_at
|
| 304 |
days = max(age.days, 0)
|
| 305 |
snapshot.warnings = [
|
| 306 |
-
f"Live AEMO BESS list
|
| 307 |
-
f"
|
| 308 |
f"({days} day{'s' if days != 1 else ''} old)."
|
| 309 |
]
|
| 310 |
_cache = (snapshot, now)
|
|
|
|
| 5 |
|
| 6 |
1. live β direct download from www.aemo.com.au (subject to WAF blocks
|
| 7 |
from datacentre IPs; see _BROWSER_HEADERS).
|
| 8 |
+
2. snapshot β the parsed bess_list.json bundled in the repo, refreshed
|
| 9 |
+
daily by .github/workflows/refresh_bess_list.yml running
|
| 10 |
+
scripts/refresh_bess_list.py from GitHub-hosted runners
|
| 11 |
+
(whose egress is not blocked by AEMO's WAF).
|
| 12 |
|
| 13 |
The first successful step is cached in memory for 24 hours. The result
|
| 14 |
carries a `source` field and any user-facing `warnings` so the API can
|
|
|
|
| 32 |
"?rev=1f6bccf827284f9fb6d6f3ae56ed3fe9&sc_lang=en"
|
| 33 |
)
|
| 34 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
# AEMO's Azure Front Door WAF on www.aemo.com.au returns 403 to requests that
|
| 36 |
# look like default Python clients. Sending a full browser-style header set
|
| 37 |
# bypasses the simpler bot-detection rules. (Doesn't help against pure
|
|
|
|
| 86 |
class BessListResult:
|
| 87 |
"""Parsed BESS list plus provenance for the API response."""
|
| 88 |
states: dict[str, list[dict]]
|
| 89 |
+
source: str # "live" | "snapshot"
|
| 90 |
+
fetched_at: datetime # for the live path: now; for snapshot: snapshot timestamp
|
| 91 |
warnings: list[str] = field(default_factory=list)
|
| 92 |
|
| 93 |
|
|
|
|
| 243 |
|
| 244 |
async def fetch_bess_list() -> BessListResult:
|
| 245 |
"""
|
| 246 |
+
Return the BESS list, trying live AEMO β bundled snapshot.
|
| 247 |
+
|
| 248 |
+
The snapshot is refreshed daily by the refresh_bess_list workflow from
|
| 249 |
+
GitHub-hosted runners (whose egress is not blocked by AEMO), so even
|
| 250 |
+
when the live AEMO fetch fails the fallback is at most ~24 hours old.
|
| 251 |
|
| 252 |
+
Uses a 24-hour in-memory cache. The result carries provenance so the
|
| 253 |
+
API can surface a warning when serving non-live data.
|
| 254 |
"""
|
| 255 |
global _cache
|
| 256 |
|
|
|
|
| 264 |
now - cached_at, cached.source)
|
| 265 |
return cached
|
| 266 |
|
| 267 |
+
# 1. Live AEMO
|
| 268 |
+
try:
|
| 269 |
+
logger.info("Downloading AEMO XLSX (live): %s", XLSX_URL)
|
| 270 |
+
async with httpx.AsyncClient(timeout=30, follow_redirects=True) as client:
|
| 271 |
states = await _download_and_parse(XLSX_URL, client)
|
| 272 |
+
result = BessListResult(states=states, source="live", fetched_at=now)
|
| 273 |
+
_cache = (result, now)
|
| 274 |
+
return result
|
| 275 |
+
except Exception as exc:
|
| 276 |
+
logger.warning("Live AEMO XLSX fetch failed: %s", exc)
|
| 277 |
|
| 278 |
+
# 2. Bundled snapshot β refreshed daily by CI from GH-hosted runners.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 279 |
try:
|
| 280 |
snapshot = _load_snapshot()
|
| 281 |
age = now - snapshot.fetched_at
|
| 282 |
days = max(age.days, 0)
|
| 283 |
snapshot.warnings = [
|
| 284 |
+
f"Live AEMO BESS list is unavailable; showing the daily snapshot "
|
| 285 |
+
f"committed on {snapshot.fetched_at:%Y-%m-%d} "
|
| 286 |
f"({days} day{'s' if days != 1 else ''} old)."
|
| 287 |
]
|
| 288 |
_cache = (snapshot, now)
|
|
@@ -3,14 +3,16 @@
|
|
| 3 |
Refresh the bundled BESS list snapshot.
|
| 4 |
|
| 5 |
Downloads the AEMO Generation Information XLSX, parses it into the same
|
| 6 |
-
shape served by /api/bess, and writes
|
|
|
|
|
|
|
| 7 |
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
|
| 15 |
Run from the repo root:
|
| 16 |
python scripts/refresh_bess_list.py
|
|
@@ -34,7 +36,6 @@ from app.services.gen_info_fetcher import XLSX_URL, _BROWSER_HEADERS, _parse_xls
|
|
| 34 |
|
| 35 |
DATA_DIR = REPO_ROOT / "app" / "data"
|
| 36 |
JSON_OUT = DATA_DIR / "bess_list.json"
|
| 37 |
-
XLSX_OUT = DATA_DIR / "aemo_gen_info.xlsx"
|
| 38 |
META_OUT = DATA_DIR / "bess_list_meta.json"
|
| 39 |
|
| 40 |
|
|
@@ -54,7 +55,6 @@ def main() -> int:
|
|
| 54 |
return 1
|
| 55 |
|
| 56 |
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
| 57 |
-
XLSX_OUT.write_bytes(xlsx_bytes)
|
| 58 |
JSON_OUT.write_text(json.dumps(parsed, indent=2, sort_keys=True) + "\n")
|
| 59 |
META_OUT.write_text(json.dumps({
|
| 60 |
"fetched_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
|
@@ -64,7 +64,6 @@ def main() -> int:
|
|
| 64 |
}, indent=2) + "\n")
|
| 65 |
|
| 66 |
print(f"Wrote {JSON_OUT.relative_to(REPO_ROOT)}")
|
| 67 |
-
print(f"Wrote {XLSX_OUT.relative_to(REPO_ROOT)}")
|
| 68 |
print(f"Wrote {META_OUT.relative_to(REPO_ROOT)}")
|
| 69 |
return 0
|
| 70 |
|
|
|
|
| 3 |
Refresh the bundled BESS list snapshot.
|
| 4 |
|
| 5 |
Downloads the AEMO Generation Information XLSX, parses it into the same
|
| 6 |
+
shape served by /api/bess, and writes app/data/bess_list.json. This file is
|
| 7 |
+
the runtime fallback used by gen_info_fetcher.py when the live AEMO fetch
|
| 8 |
+
is blocked (e.g. WAF 403 from the HuggingFace egress IP).
|
| 9 |
|
| 10 |
+
A small bess_list_meta.json sibling records when the snapshot was fetched
|
| 11 |
+
so the frontend banner can display its age.
|
| 12 |
+
|
| 13 |
+
The raw XLSX itself is intentionally NOT committed: HuggingFace Spaces
|
| 14 |
+
rejects binary files in non-Xet storage, and the parsed JSON snapshot is a
|
| 15 |
+
strictly equivalent fallback (same parser, same source).
|
| 16 |
|
| 17 |
Run from the repo root:
|
| 18 |
python scripts/refresh_bess_list.py
|
|
|
|
| 36 |
|
| 37 |
DATA_DIR = REPO_ROOT / "app" / "data"
|
| 38 |
JSON_OUT = DATA_DIR / "bess_list.json"
|
|
|
|
| 39 |
META_OUT = DATA_DIR / "bess_list_meta.json"
|
| 40 |
|
| 41 |
|
|
|
|
| 55 |
return 1
|
| 56 |
|
| 57 |
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
| 58 |
JSON_OUT.write_text(json.dumps(parsed, indent=2, sort_keys=True) + "\n")
|
| 59 |
META_OUT.write_text(json.dumps({
|
| 60 |
"fetched_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
|
|
|
| 64 |
}, indent=2) + "\n")
|
| 65 |
|
| 66 |
print(f"Wrote {JSON_OUT.relative_to(REPO_ROOT)}")
|
|
|
|
| 67 |
print(f"Wrote {META_OUT.relative_to(REPO_ROOT)}")
|
| 68 |
return 0
|
| 69 |
|