feat: backfill hii_waterlevel from the HII waterlevel_graph archive
CI/CD Pipeline - Northern Thailand Ping River Monitor / Test Suite (3.11) (push) Failing after 31s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Build Docker Image (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Integration Test with Services (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Deploy to Staging (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Deploy to Production (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Performance Test (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Cleanup (push) Successful in 0s
Documentation / Documentation Summary (push) Successful in 2s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Code Quality (push) Successful in 13s
Documentation / Validate Documentation (push) Failing after 9s
Documentation / Generate API Documentation (push) Successful in 9s
Documentation / Build Sphinx Documentation (push) Successful in 15s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Test Suite (3.11) (push) Failing after 31s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Build Docker Image (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Integration Test with Services (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Deploy to Staging (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Deploy to Production (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Performance Test (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Cleanup (push) Successful in 0s
Documentation / Documentation Summary (push) Successful in 2s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Code Quality (push) Successful in 13s
Documentation / Validate Documentation (push) Failing after 9s
Documentation / Generate API Documentation (push) Successful in 9s
Documentation / Build Sphinx Documentation (push) Successful in 15s
scripts/backfill_hii_waterlevel.py walks the api-v3 waterlevel_graph endpoint (hourly wl_msl + discharge, archive back to ~2019) in full-year windows per station and upserts into hii_waterlevel. Defaults to the RID-mirror and key stations; --stations/--all/--start/--end/--chunk-days override. History upserts touch only wl_msl and discharge so colliding live-snapshot rows keep storage_percent/situation_level. Idempotent and safe to re-run.
This commit is contained in:
@@ -4,6 +4,7 @@ import datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from src.hii_backfill import chunk_date_range, parse_graph_rows, select_stations
|
||||
from src.hii_collector import (
|
||||
HiiStore,
|
||||
parse_rain_records,
|
||||
@@ -207,3 +208,105 @@ class TestHiiStore:
|
||||
|
||||
def test_save_empty(self, store):
|
||||
assert store.save_rain([]) == 0
|
||||
|
||||
def test_history_upsert_preserves_snapshot_columns(self, store):
|
||||
# A live snapshot row exists with extra columns populated
|
||||
store.save_waterlevel(parse_waterlevel_records(_waterlevel_payload()))
|
||||
# Backfill collides on the same (station, timestamp) with new values
|
||||
rows = [
|
||||
{
|
||||
"timestamp": datetime.datetime(2026, 8, 11, 13, 0),
|
||||
"wl_msl": 303.30,
|
||||
"discharge": 340.0,
|
||||
},
|
||||
{
|
||||
"timestamp": datetime.datetime(2019, 8, 1, 1, 0),
|
||||
"wl_msl": 301.71,
|
||||
"discharge": 11.7,
|
||||
},
|
||||
]
|
||||
assert store.save_waterlevel_history(3226, rows) == 2
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
with store.engine.connect() as conn:
|
||||
collided = conn.execute(
|
||||
text(
|
||||
"SELECT wl_msl, discharge, storage_percent, situation_level "
|
||||
"FROM hii_waterlevel WHERE station_id = 3226 "
|
||||
"AND timestamp = '2026-08-11 13:00:00'"
|
||||
)
|
||||
).fetchone()
|
||||
historical = conn.execute(
|
||||
text(
|
||||
"SELECT wl_msl FROM hii_waterlevel WHERE station_id = 3226 "
|
||||
"AND timestamp = '2019-08-01 01:00:00'"
|
||||
)
|
||||
).fetchone()
|
||||
# wl_msl/discharge updated, snapshot-only columns untouched
|
||||
assert float(collided[0]) == 303.30
|
||||
assert float(collided[1]) == 340.0
|
||||
assert float(collided[2]) == 81.21
|
||||
assert collided[3] == 4
|
||||
assert float(historical[0]) == 301.71
|
||||
|
||||
|
||||
class TestParseGraphRows:
|
||||
def test_parses_and_skips_empty(self):
|
||||
payload = {
|
||||
"data": {
|
||||
"graph_data": [
|
||||
{"datetime": "2024-10-05 12:00", "value": 305.8, "discharge": 656},
|
||||
{"datetime": "2024-10-05 13:00", "value": None, "discharge": None},
|
||||
{"datetime": None, "value": 300.0, "discharge": 1},
|
||||
]
|
||||
}
|
||||
}
|
||||
rows = parse_graph_rows(payload)
|
||||
assert rows == [
|
||||
{
|
||||
"timestamp": datetime.datetime(2024, 10, 5, 12, 0),
|
||||
"wl_msl": 305.8,
|
||||
"discharge": 656.0,
|
||||
}
|
||||
]
|
||||
|
||||
def test_empty_payload(self):
|
||||
assert parse_graph_rows({}) == []
|
||||
|
||||
|
||||
class TestBackfillHelpers:
|
||||
def test_chunk_date_range(self):
|
||||
chunks = chunk_date_range(
|
||||
datetime.date(2024, 1, 1), datetime.date(2024, 3, 1), 31
|
||||
)
|
||||
assert chunks[0] == (datetime.date(2024, 1, 1), datetime.date(2024, 1, 31))
|
||||
assert chunks[-1][1] == datetime.date(2024, 3, 1)
|
||||
# Contiguous, no overlap
|
||||
for (_, prev_end), (next_start, _) in zip(chunks, chunks[1:]):
|
||||
assert next_start == prev_end + datetime.timedelta(days=1)
|
||||
|
||||
def test_chunk_single_day(self):
|
||||
d = datetime.date(2024, 1, 1)
|
||||
assert chunk_date_range(d, d, 365) == [(d, d)]
|
||||
|
||||
def test_select_default_keeps_rid_and_key_stations(self):
|
||||
records = [
|
||||
{"station_id": 1, "rid_code": "P.1", "is_key_station": True},
|
||||
{"station_id": 2, "rid_code": None, "is_key_station": False},
|
||||
{"station_id": 3, "rid_code": None, "is_key_station": True},
|
||||
]
|
||||
assert [r["station_id"] for r in select_stations(records)] == [1, 3]
|
||||
|
||||
def test_select_by_code_matches_rid_code_and_oldcode(self):
|
||||
records = [
|
||||
{"station_id": 1, "rid_code": "P.1", "oldcode": "ridhydro_P.1"},
|
||||
{"station_id": 2, "rid_code": None, "oldcode": "CHM004"},
|
||||
{"station_id": 3, "rid_code": "P.67", "oldcode": "P.67"},
|
||||
]
|
||||
selected = select_stations(records, codes=["p.1", "chm004"])
|
||||
assert [r["station_id"] for r in selected] == [1, 2]
|
||||
|
||||
def test_select_all(self):
|
||||
records = [{"station_id": 1}, {"station_id": 2}]
|
||||
assert select_stations(records, all_stations=True) == records
|
||||
|
||||
Reference in New Issue
Block a user