pourmousavi Claude Opus 4.7 (1M context) commited on
Commit
4424ac7
Β·
1 Parent(s): 0d542b0

Drop XLSX mirror layer to satisfy HuggingFace binary-file policy

Browse files

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

.github/workflows/refresh_bess_list.yml CHANGED
@@ -31,14 +31,15 @@ jobs:
31
  - name: Commit if changed
32
  run: |
33
  set -e
34
- # Only commit if the parsed JSON or raw XLSX changed.
35
- # The meta file changes every run (timestamp); ignore it on its own.
36
- if git diff --quiet -- app/data/bess_list.json app/data/aemo_gen_info.xlsx; then
37
- echo "No change in bess_list.json or aemo_gen_info.xlsx; skipping commit."
 
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/aemo_gen_info.xlsx app/data/bess_list_meta.json
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
app/services/gen_info_fetcher.py CHANGED
@@ -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. mirror β€” raw.githubusercontent.com copy committed daily by the
9
- refresh_bess_list workflow.
10
- 3. snapshot β€” the parsed bess_list.json bundled in the repo (also
11
- written by refresh_bess_list).
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" | "mirror" | "snapshot"
98
- fetched_at: datetime # for the live/mirror path: now; for snapshot: snapshot timestamp
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 β†’ GitHub mirror β†’ bundled snapshot.
 
 
 
 
255
 
256
- Uses a 24-hour in-memory cache (per source). The result carries provenance
257
- so the API can surface a warning when serving non-live data.
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
- async with httpx.AsyncClient(timeout=30, follow_redirects=True) as client:
272
- # 1. Live AEMO
273
- try:
274
- logger.info("Downloading AEMO XLSX (live): %s", XLSX_URL)
275
  states = await _download_and_parse(XLSX_URL, client)
276
- result = BessListResult(states=states, source="live", fetched_at=now)
277
- _cache = (result, now)
278
- return result
279
- except Exception as exc:
280
- logger.warning("Live AEMO XLSX fetch failed: %s", exc)
281
 
282
- # 2. GitHub raw mirror (refreshed daily by CI)
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 and mirror are both unavailable; "
307
- f"showing bundled snapshot from {snapshot.fetched_at:%Y-%m-%d} "
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)
scripts/refresh_bess_list.py CHANGED
@@ -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 two files into app/data/:
 
 
7
 
8
- bess_list.json β€” parsed snapshot used as runtime fallback when the
9
- live AEMO fetch is blocked (e.g. WAF 403 from the
10
- HuggingFace egress IP).
11
- aemo_gen_info.xlsx β€” the raw XLSX bytes, so gen_info_fetcher.py can also
12
- fall back to fetching it from raw.githubusercontent.com
13
- and parse a fresher copy at runtime.
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