Spaces:
Running
Running
Claude commited on
Commit Β·
2420f34
1
Parent(s): b7c0a6b
Fix docstring and remove wasteful D+1 fetch
Browse filesNow that fetch_csv_for_date downloads ALL files matching the settlement
date (both the _YYYYMMDD_ first-half and _2_YYYYMMDDHHMMSS_ second-half
ZIPs), the D+1 fetch in _fetch_day_pair was downloading two extra ZIP
files that the boundary filter immediately threw away, making every
request unnecessarily slow.
- Remove _fetch_day_pair; replace with _fetch_day_csv (single call)
- Update all three call sites in api.py accordingly
- Remove now-unused timedelta import from api.py
- Correct module docstring to document the actual two-file-per-day
naming convention: FPPMW_YYYYMMDD_<seq> (first half) and
FPPMW_2_YYYYMMDDHHMMSS_<seq+1> (second half), both sharing the
same settlement date string
https://claude.ai/code/session_01Vm5dTYdRf99LZ9MfB86vjx
- app/routers/api.py +15 -27
- app/services/aemo_fetcher.py +23 -10
app/routers/api.py
CHANGED
|
@@ -3,7 +3,7 @@ REST API endpoints for BESS SCADA data.
|
|
| 3 |
"""
|
| 4 |
import json
|
| 5 |
import logging
|
| 6 |
-
from datetime import date, datetime
|
| 7 |
from pathlib import Path
|
| 8 |
|
| 9 |
logger = logging.getLogger(__name__)
|
|
@@ -57,28 +57,16 @@ def get_quality_flags():
|
|
| 57 |
return json.loads(flags_file.read_text())
|
| 58 |
|
| 59 |
|
| 60 |
-
async def
|
| 61 |
"""
|
| 62 |
-
Fetch
|
| 63 |
-
|
| 64 |
-
Each FPPMW daily ZIP may contain two CSV halves covering the full NEM
|
| 65 |
-
market day (04:00β16:00 UTC and 16:00β04:00 UTC next day); both are
|
| 66 |
-
concatenated inside fetch_csv_for_date. The next-day file is also
|
| 67 |
-
fetched as a safety net for older single-CSV files and boundary coverage.
|
| 68 |
-
It is fetched with skip_future_check=True (it may be today's date) and
|
| 69 |
-
any error is silently swallowed β partial data is acceptable.
|
| 70 |
-
"""
|
| 71 |
-
import asyncio as _asyncio
|
| 72 |
-
csv_bytes = await fetch_csv_for_date(target_date, duid)
|
| 73 |
-
|
| 74 |
-
next_date = target_date + timedelta(days=1)
|
| 75 |
-
csv_next: bytes | None = None
|
| 76 |
-
try:
|
| 77 |
-
csv_next = await fetch_csv_for_date(next_date, duid, skip_future_check=True)
|
| 78 |
-
except Exception as exc:
|
| 79 |
-
logger.debug("Next-day CSV not available for %s (%s): %s", duid, next_date, exc)
|
| 80 |
|
| 81 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
|
| 84 |
@router.get("/data")
|
|
@@ -95,8 +83,8 @@ async def get_data(
|
|
| 95 |
ip = _get_ip(request)
|
| 96 |
|
| 97 |
try:
|
| 98 |
-
csv_bytes
|
| 99 |
-
df = filter_and_process(csv_bytes, duid, target_date
|
| 100 |
summary = compute_summary(df)
|
| 101 |
records = to_json_records(df)
|
| 102 |
log_request(ip, duid, date, "view")
|
|
@@ -125,8 +113,8 @@ async def download_csv(
|
|
| 125 |
ip = _get_ip(request)
|
| 126 |
|
| 127 |
try:
|
| 128 |
-
csv_bytes
|
| 129 |
-
df = filter_and_process(csv_bytes, duid, target_date
|
| 130 |
output = to_csv_bytes(df)
|
| 131 |
log_request(ip, duid, date, "download_csv")
|
| 132 |
filename = f"BESS_SCADA_{duid}_{date}.csv"
|
|
@@ -152,8 +140,8 @@ async def download_parquet(
|
|
| 152 |
ip = _get_ip(request)
|
| 153 |
|
| 154 |
try:
|
| 155 |
-
csv_bytes
|
| 156 |
-
df = filter_and_process(csv_bytes, duid, target_date
|
| 157 |
output = to_parquet_bytes(df)
|
| 158 |
log_request(ip, duid, date, "download_parquet")
|
| 159 |
filename = f"BESS_SCADA_{duid}_{date}.parquet"
|
|
|
|
| 3 |
"""
|
| 4 |
import json
|
| 5 |
import logging
|
| 6 |
+
from datetime import date, datetime
|
| 7 |
from pathlib import Path
|
| 8 |
|
| 9 |
logger = logging.getLogger(__name__)
|
|
|
|
| 57 |
return json.loads(flags_file.read_text())
|
| 58 |
|
| 59 |
|
| 60 |
+
async def _fetch_day_csv(target_date: date, duid: str) -> bytes:
|
| 61 |
"""
|
| 62 |
+
Fetch and concatenate all FPPMW ZIP files for target_date.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
+
fetch_csv_for_date already finds ALL files whose name contains the
|
| 65 |
+
settlement date (both the first and second 12-hour halves) and
|
| 66 |
+
concatenates their CSV bytes. The data_processor then applies the
|
| 67 |
+
[04:00 AEST D, 04:00 AEST D+1) boundary filter.
|
| 68 |
+
"""
|
| 69 |
+
return await fetch_csv_for_date(target_date, duid)
|
| 70 |
|
| 71 |
|
| 72 |
@router.get("/data")
|
|
|
|
| 83 |
ip = _get_ip(request)
|
| 84 |
|
| 85 |
try:
|
| 86 |
+
csv_bytes = await _fetch_day_csv(target_date, duid)
|
| 87 |
+
df = filter_and_process(csv_bytes, duid, target_date)
|
| 88 |
summary = compute_summary(df)
|
| 89 |
records = to_json_records(df)
|
| 90 |
log_request(ip, duid, date, "view")
|
|
|
|
| 113 |
ip = _get_ip(request)
|
| 114 |
|
| 115 |
try:
|
| 116 |
+
csv_bytes = await _fetch_day_csv(target_date, duid)
|
| 117 |
+
df = filter_and_process(csv_bytes, duid, target_date)
|
| 118 |
output = to_csv_bytes(df)
|
| 119 |
log_request(ip, duid, date, "download_csv")
|
| 120 |
filename = f"BESS_SCADA_{duid}_{date}.csv"
|
|
|
|
| 140 |
ip = _get_ip(request)
|
| 141 |
|
| 142 |
try:
|
| 143 |
+
csv_bytes = await _fetch_day_csv(target_date, duid)
|
| 144 |
+
df = filter_and_process(csv_bytes, duid, target_date)
|
| 145 |
output = to_parquet_bytes(df)
|
| 146 |
log_request(ip, duid, date, "download_parquet")
|
| 147 |
filename = f"BESS_SCADA_{duid}_{date}.parquet"
|
app/services/aemo_fetcher.py
CHANGED
|
@@ -7,18 +7,31 @@ File naming conventions on NEMWEB:
|
|
| 7 |
PUBLIC_NEXT_DAY_FPP_YYYYMMDD[_<suffix>].zip
|
| 8 |
ZIP contains the daily CSV directly.
|
| 9 |
|
| 10 |
-
From 11 Jan 2026 β FPPMW format (
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
Current directory (https://www.nemweb.com.au/REPORTS/Current/FPPDAILY/):
|
| 21 |
-
|
| 22 |
Keeps a long rolling history (180+ files observed).
|
| 23 |
|
| 24 |
Archive directory (https://nemweb.com.au/Reports/Archive/FPPDAILY/):
|
|
|
|
| 7 |
PUBLIC_NEXT_DAY_FPP_YYYYMMDD[_<suffix>].zip
|
| 8 |
ZIP contains the daily CSV directly.
|
| 9 |
|
| 10 |
+
From 11 Jan 2026 β FPPMW format (two ZIPs per market day):
|
| 11 |
+
Each NEM market day (04:00β04:00 AEST) is split into two 12-hour
|
| 12 |
+
halves, each published as a separate outer ZIP. Both files carry
|
| 13 |
+
the same settlement date (YYYYMMDD) in their name:
|
| 14 |
+
|
| 15 |
+
First half (04:00β16:00 AEST):
|
| 16 |
+
PUBLIC_NEXT_DAY_FPPMW_YYYYMMDD_<seq>.zip
|
| 17 |
+
|
| 18 |
+
Second half (16:00β04:00 AEST next calendar day):
|
| 19 |
+
PUBLIC_NEXT_DAY_FPPMW_2_YYYYMMDDHHMMSS_<seq+1>.zip
|
| 20 |
+
|
| 21 |
+
Both are identified by searching for "FPPMW" AND "YYYYMMDD" in the
|
| 22 |
+
directory listing. _find_files_in returns ALL matches; fetch_csv_for_date
|
| 23 |
+
downloads them all and concatenates their CSV bytes.
|
| 24 |
+
The data_processor applies the authoritative [04:00 AEST D, 04:00 AEST D+1)
|
| 25 |
+
boundary filter. All AEMO NEMWEB timestamps are in AEST (UTC+10),
|
| 26 |
+
no daylight saving.
|
| 27 |
+
|
| 28 |
+
Inner ZIP structure (each outer ZIP):
|
| 29 |
+
PUBLIC_NEXT_DAY_FPPMW_*.zip β outer ZIP (~MB)
|
| 30 |
+
PUBLIC_NEXT_DAY_FPPMW_*.ZIP β inner ZIP
|
| 31 |
+
PUBLIC_NEXT_DAY_FPPMW_*.CSV
|
| 32 |
|
| 33 |
Current directory (https://www.nemweb.com.au/REPORTS/Current/FPPDAILY/):
|
| 34 |
+
Two files per market day (FPPMW format), or one per day (FPP format).
|
| 35 |
Keeps a long rolling history (180+ files observed).
|
| 36 |
|
| 37 |
Archive directory (https://nemweb.com.au/Reports/Archive/FPPDAILY/):
|