CI/CD Pipeline - Northern Thailand Ping River Monitor / Test Suite (3.11) (push) Failing after 37s
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 / Code Quality (push) Successful in 16s
Documentation / Validate Documentation (push) Failing after 9s
Documentation / Generate API Documentation (push) Successful in 10s
Documentation / Build Sphinx Documentation (push) Successful in 15s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Cleanup (push) Successful in 0s
Documentation / Documentation Summary (push) Successful in 2s
Poll the open api-v3.thaiwater.net public endpoints (rain_24h, waterlevel_load), filter to the Ping basin, and persist to new hii_rain_stations/hii_rainfall and hii_wl_stations/hii_waterlevel tables (auto-created; sqlite/postgresql/mysql). Water levels stay in their own tables since HII reports m MSL from a different station set; rid_code maps mirrors like ridhydro_P.1 to P.1 and offset_msl converts MSL to gauge datum. Runs every scraping cycle in web-api and continuous modes (hourly cadence even during 1-minute RID retry), one-shot via --collect-hii; docs/DATA_SOURCES.md catalogs all probed endpoints.
210 lines
7.0 KiB
Python
210 lines
7.0 KiB
Python
"""Tests for the HII/ThaiWater api-v3 collector (parsing + persistence)."""
|
|
|
|
import datetime
|
|
|
|
import pytest
|
|
|
|
from src.hii_collector import (
|
|
HiiStore,
|
|
parse_rain_records,
|
|
parse_waterlevel_records,
|
|
rid_code_from_oldcode,
|
|
)
|
|
|
|
|
|
def _rain_payload():
|
|
return {
|
|
"result": "OK",
|
|
"data": [
|
|
{
|
|
"id": 306091240,
|
|
"rain_1h": 0,
|
|
"rain_24h": "49.6",
|
|
"rainfall_datetime": "2026-08-11 13:00",
|
|
"agency": {"agency_shortname": {"en": "HII", "th": "สสน."}},
|
|
"basin": {"basin_code": 6, "basin_name": {"en": "Ping Basin"}},
|
|
"station": {
|
|
"id": 418,
|
|
"sub_basin_id": "0604",
|
|
"tele_station_lat": 19.12207,
|
|
"tele_station_long": 98.94447,
|
|
"tele_station_name": {"en": "Chiang Mai 5", "th": "แม่แตง"},
|
|
"tele_station_oldcode": "CHM005",
|
|
},
|
|
},
|
|
# Wrong basin -> filtered out
|
|
{
|
|
"id": 2,
|
|
"rain_24h": 10,
|
|
"rainfall_datetime": "2026-08-11 13:00",
|
|
"basin": {"basin_code": 7},
|
|
"station": {"id": 99},
|
|
},
|
|
# No timestamp -> skipped
|
|
{
|
|
"id": 3,
|
|
"rain_24h": 5,
|
|
"rainfall_datetime": None,
|
|
"basin": {"basin_code": 6},
|
|
"station": {"id": 100},
|
|
},
|
|
],
|
|
}
|
|
|
|
|
|
def _waterlevel_payload():
|
|
return {
|
|
"waterlevel_data": {
|
|
"result": "OK",
|
|
"data": [
|
|
{
|
|
"id": 1286124160,
|
|
"waterlevel_datetime": "2026-08-11 13:00",
|
|
"waterlevel_m": None,
|
|
"waterlevel_msl": "303.27",
|
|
"discharge": "335.00",
|
|
"flow_rate": None,
|
|
"storage_percent": "81.21",
|
|
"situation_level": 4,
|
|
"diff_wl_bank": "0.93",
|
|
"river_name": "แม่น้ำปิง",
|
|
"agency": {"agency_shortname": {"en": "RID"}},
|
|
"basin": {"basin_code": 6},
|
|
"station": {
|
|
"id": 3226,
|
|
"tele_station_lat": 18.786961,
|
|
"tele_station_long": 99.005089,
|
|
"tele_station_name": {"th": "สะพานนวรัฐ"},
|
|
"tele_station_oldcode": "P.1",
|
|
"offset": 300.5,
|
|
"ground_level": 299.25,
|
|
"min_bank": 304.2,
|
|
"critical_level_msl": 304.2,
|
|
"critical_level_m": 3.7,
|
|
"qmax": 425,
|
|
"is_key_station": True,
|
|
},
|
|
},
|
|
{
|
|
"id": 2,
|
|
"waterlevel_datetime": "2026-08-11 13:00",
|
|
"waterlevel_msl": "200.0",
|
|
"basin": {"basin_code": 6},
|
|
"station": {"id": 4000, "tele_station_oldcode": "ridhydro_P.67"},
|
|
},
|
|
# Wrong basin -> filtered out
|
|
{
|
|
"id": 3,
|
|
"waterlevel_datetime": "2026-08-11 13:00",
|
|
"basin": {"basin_code": 10},
|
|
"station": {"id": 5000},
|
|
},
|
|
],
|
|
}
|
|
}
|
|
|
|
|
|
class TestRidCodeNormalization:
|
|
def test_plain_code(self):
|
|
assert rid_code_from_oldcode("P.1") == "P.1"
|
|
|
|
def test_ridhydro_prefix(self):
|
|
assert rid_code_from_oldcode("ridhydro_P.67") == "P.67"
|
|
|
|
def test_letter_suffix(self):
|
|
assert rid_code_from_oldcode("ridhydro_P.4A") == "P.4A"
|
|
|
|
def test_non_rid_codes(self):
|
|
assert rid_code_from_oldcode("CHM005") is None
|
|
assert rid_code_from_oldcode("ridtele_TUP.14") is None
|
|
assert rid_code_from_oldcode(None) is None
|
|
|
|
|
|
class TestParseRain:
|
|
def test_filters_and_parses(self):
|
|
records = parse_rain_records(_rain_payload())
|
|
assert len(records) == 1
|
|
r = records[0]
|
|
assert r["station_id"] == 418
|
|
assert r["oldcode"] == "CHM005"
|
|
assert r["name_en"] == "Chiang Mai 5"
|
|
assert r["rain_1h"] == 0.0
|
|
assert r["rain_24h"] == 49.6
|
|
assert r["timestamp"] == datetime.datetime(2026, 8, 11, 13, 0)
|
|
assert r["agency"] == "HII"
|
|
|
|
def test_empty_payload(self):
|
|
assert parse_rain_records({}) == []
|
|
|
|
|
|
class TestParseWaterlevel:
|
|
def test_filters_and_parses(self):
|
|
records = parse_waterlevel_records(_waterlevel_payload())
|
|
assert len(records) == 2
|
|
p1 = records[0]
|
|
assert p1["station_id"] == 3226
|
|
assert p1["rid_code"] == "P.1"
|
|
assert p1["wl_msl"] == 303.27
|
|
assert p1["discharge"] == 335.0
|
|
assert p1["flow_rate"] is None
|
|
assert p1["storage_percent"] == 81.21
|
|
assert p1["situation_level"] == 4
|
|
assert p1["offset_msl"] == 300.5
|
|
assert p1["is_key_station"] is True
|
|
# MSL minus station offset recovers the familiar gauge level
|
|
assert p1["wl_msl"] - p1["offset_msl"] == pytest.approx(2.77)
|
|
assert records[1]["rid_code"] == "P.67"
|
|
|
|
def test_empty_payload(self):
|
|
assert parse_waterlevel_records({}) == []
|
|
|
|
|
|
class TestHiiStore:
|
|
@pytest.fixture
|
|
def store(self, tmp_path):
|
|
store = HiiStore(f"sqlite:///{tmp_path}/hii_test.db", "sqlite")
|
|
assert store.connect()
|
|
return store
|
|
|
|
def test_rejects_non_sql_backend(self):
|
|
with pytest.raises(ValueError):
|
|
HiiStore("http://localhost:8428", "victoriametrics")
|
|
|
|
def test_rain_roundtrip_and_upsert(self, store):
|
|
records = parse_rain_records(_rain_payload())
|
|
assert store.save_rain(records) == 1
|
|
# Same snapshot again -> upsert, still one row
|
|
assert store.save_rain(records) == 1
|
|
|
|
from sqlalchemy import text
|
|
|
|
with store.engine.connect() as conn:
|
|
rows = conn.execute(text("SELECT COUNT(*) FROM hii_rainfall")).scalar()
|
|
stations = conn.execute(
|
|
text("SELECT oldcode FROM hii_rain_stations")
|
|
).fetchall()
|
|
assert rows == 1
|
|
assert stations == [("CHM005",)]
|
|
|
|
def test_waterlevel_roundtrip(self, store):
|
|
records = parse_waterlevel_records(_waterlevel_payload())
|
|
assert store.save_waterlevel(records) == 2
|
|
|
|
from sqlalchemy import text
|
|
|
|
with store.engine.connect() as conn:
|
|
row = conn.execute(
|
|
text(
|
|
"SELECT s.rid_code, m.wl_msl, m.situation_level "
|
|
"FROM hii_waterlevel m JOIN hii_wl_stations s ON s.id = m.station_id "
|
|
"WHERE s.oldcode = 'P.1'"
|
|
)
|
|
).fetchone()
|
|
assert row is not None
|
|
assert row[0] == "P.1"
|
|
assert float(row[1]) == 303.27
|
|
assert row[2] == 4
|
|
|
|
def test_save_empty(self, store):
|
|
assert store.save_rain([]) == 0
|