mirror of
https://github.com/NomaDamas/k-skill.git
synced 2026-06-24 02:04:11 +00:00
Honor documented fine-dust fallback semantics
The fine-dust helper still broke the documented station-name path when the station lookup API returned no rows, and it guessed a KHAI label from PM10 whenever the upstream API omitted khaiGrade. This change locks both behaviors with regressions, resolves station selection to a direct measurement lookup when only the station name is usable, and reports 정보없음 when the source omits the KHAI field. The docs now match the implemented fallback and missing-field behavior. Constraint: Keep the fix compatible with existing fetch_station_payload callers and PR #21 scope Rejected: Recompute KHAI from other pollutant fields | official formula/data inputs are not implemented in this helper Rejected: Fail hard when station lookup is empty despite --station-name | contradicts the documented direct station-name fallback Confidence: high Scope-risk: narrow Reversibility: clean Directive: Do not synthesize 통합대기등급 from PM10/PM2.5 surrogates unless the official KHAI formula and inputs are implemented end-to-end Tested: python3 scripts/test_fine_dust.py; npm run ci; python3 scripts/fine_dust.py report --station-file scripts/fixtures/fine-dust-stations.json --measurement-file scripts/fixtures/fine-dust-measurements.json --lat 37.5665 --lon 126.9780; python3 scripts/fine_dust.py report --station-file scripts/fixtures/fine-dust-stations.json --measurement-file scripts/fixtures/fine-dust-measurements.json --region-hint '서울 강남구'; python3 scripts/fine_dust.py --help; env -u AIR_KOREA_OPEN_API_KEY python3 scripts/fine_dust.py report --lat 37.5665 --lon 126.9780; python3 -m py_compile scripts/fine_dust.py scripts/test_fine_dust.py Not-tested: Live Air Korea API responses with a real service key
This commit is contained in:
parent
3237d9dc2f
commit
9ef274c3b7
4 changed files with 97 additions and 4 deletions
|
|
@ -89,6 +89,7 @@ python3 scripts/fine_dust.py report \
|
|||
|
||||
- 위치 권한이 없으면 지역명/행정구역을 먼저 받습니다
|
||||
- 지역명도 없으면 측정소명을 직접 받습니다
|
||||
- 측정소 목록 API가 빈 응답이어도 `--station-name` 이 있으면 같은 이름으로 실시간 측정 API를 직접 재시도합니다
|
||||
- `getNearbyMsrstnList` 결과가 비면 `getMsrstnList` 로 재시도합니다
|
||||
- nearby 응답은 입력 TM 좌표와의 거리 기준으로 정렬되므로 첫 측정소를 우선 사용합니다
|
||||
|
||||
|
|
@ -96,4 +97,5 @@ python3 scripts/fine_dust.py report \
|
|||
|
||||
- 실시간 수치라 조회 시각을 같이 적어야 합니다
|
||||
- PM10/PM2.5 값이 `-` 이거나 비정상이면 등급도 함께 재확인합니다
|
||||
- API 가 `khaiGrade` 를 비워 보내면 통합대기등급은 `정보없음` 으로 표시합니다
|
||||
- 위치 기반이라고 해도 실제 기준은 “가까운 측정소” 이므로 주소 중심점과 오차가 있을 수 있습니다
|
||||
|
|
|
|||
|
|
@ -94,6 +94,8 @@ sops exec-env "$HOME/.config/k-skill/secrets.env" \
|
|||
2. 지역명/행정구역 → `getMsrstnList`
|
||||
3. 측정소명 직접 지정 → `getMsrstnAcctoRltmMesureDnsty`
|
||||
|
||||
`getMsrstnList` 가 빈 응답이어도 `--station-name` 이 있으면 helper 는 같은 이름으로 `getMsrstnAcctoRltmMesureDnsty` 를 직접 재시도한다.
|
||||
|
||||
### 4. Query the official real-time measurement API
|
||||
|
||||
선택한 가까운 측정소 이름으로 대기오염정보 API `getMsrstnAcctoRltmMesureDnsty` 를 호출해 PM10/PM2.5 와 등급을 가져온다.
|
||||
|
|
@ -134,6 +136,7 @@ python3 scripts/fine_dust.py report \
|
|||
- PM10 값과 등급
|
||||
- PM2.5 값과 등급
|
||||
- 좌표 기반 조회인지, 지역 fallback인지
|
||||
- `khaiGrade` 가 비어 있으면 통합대기등급은 `정보없음`
|
||||
|
||||
## Done when
|
||||
|
||||
|
|
|
|||
|
|
@ -221,6 +221,29 @@ def pick_station(
|
|||
return station_items[0]
|
||||
|
||||
|
||||
def resolve_station(
|
||||
station_items: list[dict],
|
||||
*,
|
||||
lat: float | None = None,
|
||||
lon: float | None = None,
|
||||
region_hint: str | None = None,
|
||||
station_name: str | None = None,
|
||||
) -> dict:
|
||||
if station_items:
|
||||
return pick_station(
|
||||
station_items,
|
||||
lat=lat,
|
||||
lon=lon,
|
||||
region_hint=region_hint,
|
||||
station_name=station_name,
|
||||
)
|
||||
|
||||
if station_name:
|
||||
return {"stationName": station_name, "addr": None}
|
||||
|
||||
raise SystemExit("측정소 후보가 없습니다.")
|
||||
|
||||
|
||||
def find_measurement(measurement_items: list[dict], station_name: str) -> dict:
|
||||
exact_match = next((item for item in measurement_items if item.get("stationName") == station_name), None)
|
||||
if exact_match:
|
||||
|
|
@ -265,8 +288,9 @@ def build_report(
|
|||
region_hint: str | None = None,
|
||||
station_name: str | None = None,
|
||||
lookup_mode: str | None = None,
|
||||
selected_station: dict | None = None,
|
||||
) -> dict:
|
||||
station = pick_station(
|
||||
station = selected_station or resolve_station(
|
||||
station_items,
|
||||
lat=lat,
|
||||
lon=lon,
|
||||
|
|
@ -298,7 +322,9 @@ def build_report(
|
|||
value=measurement.get("pm25Value"),
|
||||
),
|
||||
},
|
||||
"khai_grade": grade_to_label(
|
||||
"khai_grade": "정보없음"
|
||||
if measurement.get("khaiGrade") in (None, "")
|
||||
else grade_to_label(
|
||||
measurement.get("khaiGrade"),
|
||||
pollutant="pm10",
|
||||
value=measurement.get("pm10Value"),
|
||||
|
|
@ -399,7 +425,7 @@ def render_text(report: dict) -> str:
|
|||
return "\n".join(
|
||||
[
|
||||
f"측정소: {report['station_name']}",
|
||||
f"주소: {report['station_address']}",
|
||||
f"주소: {report['station_address'] or '-'}",
|
||||
f"조회 시각: {report['measured_at']}",
|
||||
f"조회 방식: {report['lookup_mode']}",
|
||||
f"PM10: {report['pm10']['value']} ({report['pm10']['grade']})",
|
||||
|
|
@ -412,7 +438,7 @@ def render_text(report: dict) -> str:
|
|||
def command_report(args: argparse.Namespace) -> None:
|
||||
station_payload, lookup_mode = fetch_station_lookup(args)
|
||||
station_items = extract_items(station_payload)
|
||||
station = pick_station(
|
||||
station = resolve_station(
|
||||
station_items,
|
||||
lat=args.lat,
|
||||
lon=args.lon,
|
||||
|
|
@ -429,6 +455,7 @@ def command_report(args: argparse.Namespace) -> None:
|
|||
region_hint=args.region_hint,
|
||||
station_name=station["stationName"],
|
||||
lookup_mode=lookup_mode,
|
||||
selected_station=station,
|
||||
)
|
||||
|
||||
if args.json:
|
||||
|
|
|
|||
|
|
@ -69,6 +69,25 @@ class FineDustTests(unittest.TestCase):
|
|||
self.assertEqual(report["pm25"], {"value": "19", "grade": "보통"})
|
||||
self.assertEqual(report["measured_at"], "2026-03-27 21:00")
|
||||
|
||||
def test_build_report_marks_khai_grade_unknown_when_api_omits_it(self):
|
||||
report = fine_dust.build_report(
|
||||
station_items=[{"stationName": "중구", "addr": "서울 중구 서소문로 124"}],
|
||||
measurement_items=[
|
||||
{
|
||||
"stationName": "중구",
|
||||
"dataTime": "2026-03-27 21:00",
|
||||
"pm10Value": "42",
|
||||
"pm10Grade": "2",
|
||||
"pm25Value": "19",
|
||||
"pm25Grade": "2",
|
||||
"khaiGrade": None,
|
||||
}
|
||||
],
|
||||
station_name="중구",
|
||||
)
|
||||
|
||||
self.assertEqual(report["khai_grade"], "정보없음")
|
||||
|
||||
def test_cli_report_supports_fixture_inputs(self):
|
||||
station_path = FIXTURES / "fine-dust-stations.json"
|
||||
measurement_path = FIXTURES / "fine-dust-measurements.json"
|
||||
|
|
@ -191,6 +210,48 @@ class FineDustTests(unittest.TestCase):
|
|||
self.assertEqual(rendered["station_name"], "강남구")
|
||||
self.assertEqual(rendered["lookup_mode"], "fallback")
|
||||
|
||||
def test_cli_json_report_uses_station_name_directly_when_station_lookup_is_empty(self):
|
||||
stdout = io.StringIO()
|
||||
recorded_calls = []
|
||||
|
||||
def fake_fetch_json(url, params):
|
||||
recorded_calls.append((url, params))
|
||||
if url.endswith("/getMsrstnList"):
|
||||
return {"response": {"body": {"items": []}}}
|
||||
if url.endswith("/getMsrstnAcctoRltmMesureDnsty"):
|
||||
return {
|
||||
"response": {
|
||||
"body": {
|
||||
"items": [
|
||||
{
|
||||
"stationName": "중구",
|
||||
"dataTime": "2026-03-27 21:00",
|
||||
"pm10Value": "42",
|
||||
"pm10Grade": "2",
|
||||
"pm25Value": "19",
|
||||
"pm25Grade": "2",
|
||||
"khaiGrade": "2",
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
raise AssertionError(f"unexpected URL: {url}")
|
||||
|
||||
with (
|
||||
redirect_stdout(stdout),
|
||||
mock.patch.object(fine_dust, "get_required_secret", return_value="test-secret"),
|
||||
mock.patch.object(fine_dust, "fetch_json", side_effect=fake_fetch_json),
|
||||
):
|
||||
fine_dust.main(["report", "--station-name", "중구", "--json"])
|
||||
|
||||
rendered = json.loads(stdout.getvalue())
|
||||
self.assertEqual(rendered["station_name"], "중구")
|
||||
self.assertIsNone(rendered["station_address"])
|
||||
self.assertEqual(rendered["lookup_mode"], "fallback")
|
||||
self.assertEqual([url.rsplit("/", 1)[-1] for url, _ in recorded_calls], ["getMsrstnList", "getMsrstnAcctoRltmMesureDnsty"])
|
||||
self.assertEqual(recorded_calls[1][1]["stationName"], "중구")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue