mirror of
https://github.com/NomaDamas/k-skill.git
synced 2026-06-24 02:04:11 +00:00
Make KTX seat summaries match active filters
Constraint: PR #296 follow-up must address reproducible review findings without live Korail credentials. Rejected: Keeping available_seats unfiltered | It conflicted with --power-only user intent and review feedback. Confidence: high Scope-risk: narrow Directive: Keep filtered availability and all_available_* semantics documented together when changing seats output. Tested: PYTHONPATH=.:scripts PYTHONNOUSERSITE=1 python3 -m unittest discover -s scripts -p 'test_ktx_booking.py'; PYTHONPATH=.:scripts python3 -m unittest scripts.test_ktx_booking; python3 -m py_compile scripts/ktx_booking.py scripts/test_ktx_booking.py; node --check scripts/skill-docs.test.js; node --test scripts/skill-docs.test.js; git diff --check origin/dev...HEAD; ruff check scripts/ktx_booking.py scripts/test_ktx_booking.py; PYTHONPATH=.:scripts python3 scripts/ktx_booking.py seats --help Not-tested: live authenticated Korail seat lookup
This commit is contained in:
parent
6a26a194c6
commit
002c6c13bc
4 changed files with 55 additions and 17 deletions
|
|
@ -106,7 +106,7 @@ python3 scripts/ktx_booking.py seats 남춘천 용산 20260503 150000 \
|
|||
--available-only
|
||||
```
|
||||
|
||||
`seats` 응답은 호차별 `remaining_seats`, `available_seats`, 좌석별 순방향/역방향, 창측/내측, 좌석 종류, 문 근처 여부, 콘센트 힌트를 JSON 으로 반환한다. 호차 요약에는 잔여석이 있는데 좌석 상세 응답 형식이 깨졌다면 해당 호차에 `seat_lookup_error` 를 포함하고 `remaining_seats` 를 보존한다. 이 단계는 좌석을 선택하거나 선점하지 않고, 예약 전 확인만 한다.
|
||||
`seats` 응답은 호차별 `remaining_seats`, 활성 필터에 맞는 `available_seats`/`available_seat_count`, 필터 전 전체 잔여 좌석 요약인 `all_available_seats`/`all_available_seat_count`, 좌석별 순방향/역방향, 창측/내측, 좌석 종류, 문 근처 여부, 콘센트 힌트를 JSON 으로 반환한다. 예를 들어 `--power-only` 를 쓰면 `available_seats` 는 콘센트 힌트가 있는 좌석만 요약하고, `all_available_seats` 는 필터 전 잔여 좌석을 보존한다. 호차 요약에는 잔여석이 있는데 좌석 상세 응답 형식이 깨졌다면 해당 호차에 `seat_lookup_error` 를 포함하고 `remaining_seats` 를 보존한다. 이 단계는 좌석을 선택하거나 선점하지 않고, 예약 전 확인만 한다.
|
||||
|
||||
예약:
|
||||
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ python3 scripts/ktx_booking.py seats 남춘천 용산 20260503 150000 \
|
|||
상세 좌석 응답을 보여줄 때는 사용자 의도에 맞춰 아래를 우선 요약한다.
|
||||
|
||||
- 호차별 `remaining_seats`, `available_seat_count`
|
||||
- 남은 좌석 번호 (`available_seats`)
|
||||
- 활성 필터에 맞는 남은 좌석 번호 (`available_seats`)와 필터 전 전체 잔여 좌석 번호 (`all_available_seats`)
|
||||
- 좌석별 `direction`, `position`, `seat_type`
|
||||
- 콘센트 힌트 (`power_outlet`)
|
||||
- 문 근처 여부 (`near_door`)
|
||||
|
|
|
|||
|
|
@ -558,7 +558,19 @@ class PatchedKorail(Korail):
|
|||
return {}
|
||||
|
||||
def _seat_lookup_payload(self, raw_train: dict[str, object], passenger_count: int, room_class: str) -> dict[str, object]:
|
||||
missing_fields = [field for field in SEAT_LOOKUP_FIELD_MAP if not str(raw_train.get(field, ""))]
|
||||
seat_context: dict[str, str] = {}
|
||||
missing_fields: list[str] = []
|
||||
for field, payload_name in SEAT_LOOKUP_FIELD_MAP.items():
|
||||
value = raw_train.get(field)
|
||||
if value is None:
|
||||
missing_fields.append(field)
|
||||
continue
|
||||
normalized_value = str(value).strip()
|
||||
if not normalized_value:
|
||||
missing_fields.append(field)
|
||||
continue
|
||||
seat_context[payload_name] = normalized_value
|
||||
|
||||
if missing_fields:
|
||||
raise make_korail_error(
|
||||
"seat lookup context missing "
|
||||
|
|
@ -567,10 +579,7 @@ class PatchedKorail(Korail):
|
|||
"KSKILL_SEAT_CONTEXT",
|
||||
)
|
||||
|
||||
payload = {
|
||||
payload_name: str(raw_train[field])
|
||||
for field, payload_name in SEAT_LOOKUP_FIELD_MAP.items()
|
||||
}
|
||||
payload: dict[str, object] = dict(seat_context)
|
||||
payload.update({
|
||||
"Device": self._device,
|
||||
"Version": self._version,
|
||||
|
|
@ -956,24 +965,28 @@ def command_seats(args: argparse.Namespace) -> None:
|
|||
car_payloads: list[dict[str, object]] = []
|
||||
for car in cars:
|
||||
raw = client.car_seats(raw_train, str(car["car_no_raw"]), passenger_count, room_class)
|
||||
raw_seats, seat_lookup_error = extract_car_seat_rows(raw, int(car["remaining_seats"]))
|
||||
remaining_seats = int(str(car["remaining_seats"]))
|
||||
raw_seats, seat_lookup_error = extract_car_seat_rows(raw, remaining_seats)
|
||||
all_seats = [
|
||||
normalize_seat(seat)
|
||||
for seat in raw_seats
|
||||
if seat.get("h_con_seat_no") != "0A"
|
||||
]
|
||||
available_seats = [seat for seat in all_seats if seat["available"]]
|
||||
seats = all_seats
|
||||
all_available_seats = [seat for seat in all_seats if seat["available"]]
|
||||
matching_seats = all_seats
|
||||
if args.available_only:
|
||||
seats = available_seats
|
||||
matching_seats = all_available_seats
|
||||
if args.power_only:
|
||||
seats = [seat for seat in seats if seat["power_outlet"] != "none"]
|
||||
seats = seats[: args.limit]
|
||||
matching_seats = [seat for seat in matching_seats if seat["power_outlet"] != "none"]
|
||||
matching_available_seats = [seat for seat in matching_seats if seat["available"]]
|
||||
seats = matching_seats[: args.limit]
|
||||
car_payload = dict(car)
|
||||
if seat_lookup_error:
|
||||
car_payload["seat_lookup_error"] = seat_lookup_error
|
||||
car_payload["available_seat_count"] = None if seat_lookup_error else len(available_seats)
|
||||
car_payload["available_seats"] = [seat["seat"] for seat in available_seats]
|
||||
car_payload["all_available_seat_count"] = None if seat_lookup_error else len(all_available_seats)
|
||||
car_payload["all_available_seats"] = [seat["seat"] for seat in all_available_seats]
|
||||
car_payload["available_seat_count"] = None if seat_lookup_error else len(matching_available_seats)
|
||||
car_payload["available_seats"] = [seat["seat"] for seat in matching_available_seats]
|
||||
car_payload["shown_seat_count"] = len(seats)
|
||||
car_payload["seats"] = seats
|
||||
car_payloads.append(car_payload)
|
||||
|
|
|
|||
|
|
@ -349,6 +349,29 @@ class KtxBookingTests(unittest.TestCase):
|
|||
self.assertIn("h_arv_rs_stn_cd", message)
|
||||
self.assertIn("h_trn_gp_cd", message)
|
||||
|
||||
def test_seat_lookup_payload_rejects_none_and_blank_context_fields(self):
|
||||
client = object.__new__(ktx_booking.PatchedKorail)
|
||||
client._device = "AD"
|
||||
client._version = "240906001"
|
||||
client._key = "session-key"
|
||||
raw_train: dict[str, object] = {
|
||||
field: "context-value"
|
||||
for field in ktx_booking.SEAT_LOOKUP_FIELD_MAP
|
||||
}
|
||||
bad_values: tuple[object, ...] = (None, "", " ")
|
||||
|
||||
for bad_value in bad_values:
|
||||
with self.subTest(bad_value=bad_value):
|
||||
raw_train["h_trn_no"] = bad_value
|
||||
with self.assertRaises(ktx_booking.KorailError) as exc:
|
||||
client._seat_lookup_payload(raw_train, 1, "1")
|
||||
|
||||
message = str(exc.exception)
|
||||
self.assertIn("seat lookup context missing", message)
|
||||
self.assertIn("h_trn_no", message)
|
||||
self.assertNotIn("txtTrnNo", message)
|
||||
raw_train["h_trn_no"] = "009"
|
||||
|
||||
def test_command_search_replays_selected_train_type(self):
|
||||
selected = FakeTrain(
|
||||
train_no="2080",
|
||||
|
|
@ -541,8 +564,10 @@ class KtxBookingTests(unittest.TestCase):
|
|||
self.assertEqual(len(result["cars"]), 1)
|
||||
self.assertEqual(result["cars"][0]["car_no"], 5)
|
||||
self.assertEqual(result["cars"][0]["remaining_seats"], 3)
|
||||
self.assertEqual(result["cars"][0]["available_seat_count"], 2)
|
||||
self.assertEqual(result["cars"][0]["available_seats"], ["1A", "2A"])
|
||||
self.assertEqual(result["cars"][0]["all_available_seat_count"], 2)
|
||||
self.assertEqual(result["cars"][0]["all_available_seats"], ["1A", "2A"])
|
||||
self.assertEqual(result["cars"][0]["available_seat_count"], 1)
|
||||
self.assertEqual(result["cars"][0]["available_seats"], ["1A"])
|
||||
self.assertEqual(result["cars"][0]["shown_seat_count"], 1)
|
||||
self.assertEqual(result["cars"][0]["seats"][0]["seat"], "1A")
|
||||
self.assertEqual(result["cars"][0]["seats"][0]["power_outlet"], "direct")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue