mirror of
https://github.com/NomaDamas/k-skill.git
synced 2026-06-24 02:04:11 +00:00
Keep PR #19 CI stable without user Python site-packages
The validate job was failing because the KTX helper regression suite imported optional runtime dependencies directly from the module top level. GitHub Actions does not inherit the local user site-packages that made those imports succeed on the workstation, so the test suite died before it could exercise the helper logic. This change makes the helper import-safe in minimal environments by deferring requests usage, providing lightweight fallbacks for optional Korail/Crypto imports during unit tests, and surfacing an explicit install command when the real runtime dependencies are actually needed. The docs now list pycryptodome alongside korail2, and the regression suite forces PYTHONNOUSERSITE=1 so CI keeps exercising the dependency-light path instead of accidentally relying on a developer machine. Constraint: PR #19 must keep npm run ci green on GitHub Actions without assuming user-level Python packages Constraint: The KTX helper still needs the real korail2 and pycryptodome packages for live reservation flows Rejected: Installing ad-hoc Python packages in the CI workflow | hides the import-safety regression instead of fixing the helper/test contract Rejected: Removing the Python regression suite from skill-docs coverage | would lose the guard on the train_id reservation flow Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep the KTX helper importable under PYTHONNOUSERSITE=1 and document every required runtime package in both the skill and install docs Tested: PYTHONNOUSERSITE=1 python3 -m unittest discover -s scripts -p test_ktx_booking.py Tested: node --test scripts/skill-docs.test.js Tested: npm run ci Not-tested: GitHub Actions validate rerun after push
This commit is contained in:
parent
5fb1b7a57f
commit
1e13cc10e1
5 changed files with 116 additions and 26 deletions
|
|
@ -11,7 +11,7 @@
|
|||
## 먼저 필요한 것
|
||||
|
||||
- Python 3.10+
|
||||
- `python3 -m pip install korail2`
|
||||
- `python3 -m pip install korail2 pycryptodome`
|
||||
- [공통 설정 가이드](../setup.md) 완료
|
||||
- [보안/시크릿 정책](../security-and-secrets.md) 확인
|
||||
|
||||
|
|
@ -43,7 +43,7 @@
|
|||
|
||||
## 기본 흐름
|
||||
|
||||
1. `korail2` 패키지가 없으면 다른 방법으로 우회하지 말고 먼저 전역 설치한다.
|
||||
1. `korail2` 또는 `pycryptodome` 패키지가 없으면 다른 방법으로 우회하지 말고 먼저 전역 설치한다.
|
||||
2. `KSKILL_KTX_ID`, `KSKILL_KTX_PASSWORD` 가 없으면 채팅에 붙여 넣게 하지 말고 로컬 secrets 등록 절차를 안내한다.
|
||||
3. helper 로 먼저 열차를 조회한다.
|
||||
4. 후보 열차의 `index`, `train_id`, 출발/도착 시각, KTX 여부, 좌석 여부를 보여준다.
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ brew install silver-flight-group/tap/kakaocli
|
|||
### Python 패키지
|
||||
|
||||
```bash
|
||||
python3 -m pip install SRTrain korail2
|
||||
python3 -m pip install SRTrain korail2 pycryptodome
|
||||
```
|
||||
|
||||
운영체제 정책이나 권한 때문에 전역 설치가 막히면, 임의의 대체 구현으로 넘어가지 말고 그 차단 사유를 사용자에게 설명한 뒤 다음 설치 단계를 정합니다.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
name: ktx-booking
|
||||
description: Search, reserve, inspect, and cancel KTX or Korail tickets in Korea with the korail2 Python package. Use when the user asks for KTX seats, Korail bookings, train changes, or reservation status.
|
||||
description: Search, reserve, inspect, and cancel KTX or Korail tickets in Korea with the korail2 + pycryptodome Python packages. Use when the user asks for KTX seats, Korail bookings, train changes, or reservation status.
|
||||
license: MIT
|
||||
metadata:
|
||||
category: travel
|
||||
|
|
@ -32,7 +32,7 @@ metadata:
|
|||
## Prerequisites
|
||||
|
||||
- Python 3.10+
|
||||
- `python3 -m pip install korail2`
|
||||
- `python3 -m pip install korail2 pycryptodome`
|
||||
- `sops` and `age` installed
|
||||
- common setup reviewed in `../k-skill-setup/SKILL.md`
|
||||
- secret policy reviewed in `../docs/security-and-secrets.md`
|
||||
|
|
@ -56,10 +56,10 @@ metadata:
|
|||
|
||||
### 0. Install the package globally when missing
|
||||
|
||||
`python3 -c 'import korail2'` 가 실패하면 다른 구현으로 우회하지 말고 전역 Python 패키지 설치를 먼저 시도한다.
|
||||
`python3 -c 'import korail2, Crypto'` 가 실패하면 다른 구현으로 우회하지 말고 전역 Python 패키지 설치를 먼저 시도한다.
|
||||
|
||||
```bash
|
||||
python3 -m pip install korail2
|
||||
python3 -m pip install korail2 pycryptodome
|
||||
```
|
||||
|
||||
### 1. Stop for secure registration when secrets are missing
|
||||
|
|
|
|||
|
|
@ -6,29 +6,97 @@ import base64
|
|||
import json
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import string
|
||||
import sys
|
||||
import time
|
||||
from functools import reduce
|
||||
|
||||
import requests
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Util.Padding import pad
|
||||
from korail2 import (
|
||||
AdultPassenger,
|
||||
ChildPassenger,
|
||||
Korail,
|
||||
KorailError,
|
||||
NeedToLoginError,
|
||||
NoResultsError,
|
||||
Passenger,
|
||||
ReserveOption,
|
||||
SeniorPassenger,
|
||||
SoldOutError,
|
||||
ToddlerPassenger,
|
||||
TrainType,
|
||||
)
|
||||
import korail2.korail2 as korail_mod
|
||||
try:
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Util.Padding import pad
|
||||
except ModuleNotFoundError as exc:
|
||||
AES = None
|
||||
pad = None
|
||||
_CRYPTO_IMPORT_ERROR = exc
|
||||
else:
|
||||
_CRYPTO_IMPORT_ERROR = None
|
||||
|
||||
try:
|
||||
from korail2 import (
|
||||
AdultPassenger,
|
||||
ChildPassenger,
|
||||
Korail,
|
||||
KorailError,
|
||||
NeedToLoginError,
|
||||
NoResultsError,
|
||||
Passenger,
|
||||
ReserveOption,
|
||||
SeniorPassenger,
|
||||
SoldOutError,
|
||||
ToddlerPassenger,
|
||||
TrainType,
|
||||
)
|
||||
import korail2.korail2 as korail_mod
|
||||
except ModuleNotFoundError as exc:
|
||||
_KORAIL_IMPORT_ERROR = exc
|
||||
|
||||
class KorailError(Exception):
|
||||
pass
|
||||
|
||||
class NeedToLoginError(KorailError):
|
||||
pass
|
||||
|
||||
class NoResultsError(KorailError):
|
||||
pass
|
||||
|
||||
class SoldOutError(KorailError):
|
||||
pass
|
||||
|
||||
class Passenger:
|
||||
def __init__(self, count: int = 1):
|
||||
self.count = count
|
||||
|
||||
@staticmethod
|
||||
def reduce(passengers):
|
||||
return passengers
|
||||
|
||||
def get_dict(self, _: int) -> dict[str, str]:
|
||||
return {}
|
||||
|
||||
class AdultPassenger(Passenger):
|
||||
pass
|
||||
|
||||
class ChildPassenger(Passenger):
|
||||
pass
|
||||
|
||||
class ToddlerPassenger(Passenger):
|
||||
pass
|
||||
|
||||
class SeniorPassenger(Passenger):
|
||||
pass
|
||||
|
||||
class ReserveOption:
|
||||
GENERAL_FIRST = "GENERAL_FIRST"
|
||||
GENERAL_ONLY = "GENERAL_ONLY"
|
||||
SPECIAL_FIRST = "SPECIAL_FIRST"
|
||||
SPECIAL_ONLY = "SPECIAL_ONLY"
|
||||
|
||||
class TrainType:
|
||||
ALL = "ALL"
|
||||
KTX = "KTX"
|
||||
|
||||
class Korail:
|
||||
def __init__(self, *args, **kwargs):
|
||||
raise ModuleNotFoundError("korail2")
|
||||
|
||||
class _FallbackKorailModule:
|
||||
EMAIL_REGEX = re.compile(r".+@.+")
|
||||
PHONE_NUMBER_REGEX = re.compile(r"^\d+$")
|
||||
|
||||
korail_mod = _FallbackKorailModule()
|
||||
else:
|
||||
_KORAIL_IMPORT_ERROR = None
|
||||
|
||||
DEFAULT_USER_AGENT = "Dalvik/2.1.0 (Linux; U; Android 13; SM-S928N Build/UP1A.231005.007)"
|
||||
DYNAPATH_PATHS = [
|
||||
|
|
@ -61,6 +129,20 @@ TRAIN_ID_FIELDS = (
|
|||
)
|
||||
|
||||
|
||||
def ensure_runtime_dependencies() -> None:
|
||||
missing: list[str] = []
|
||||
if _KORAIL_IMPORT_ERROR is not None:
|
||||
missing.append("korail2")
|
||||
if _CRYPTO_IMPORT_ERROR is not None:
|
||||
missing.append("pycryptodome")
|
||||
if missing:
|
||||
install_command = f"python3 -m pip install {' '.join(missing)}"
|
||||
raise SystemExit(
|
||||
"scripts/ktx_booking.py requires additional Python packages "
|
||||
f"({', '.join(missing)}). Install them before running this helper: {install_command}"
|
||||
)
|
||||
|
||||
|
||||
class DynaPathMasterEngine:
|
||||
APP_ID = "com.korail.talk"
|
||||
AS_VALUE = "%5B38ff229cb34c7dda8e28220a2d750cce%5D"
|
||||
|
|
@ -178,6 +260,8 @@ class PatchedKorail(Korail):
|
|||
_device_id = "558a4f02041657ea"
|
||||
|
||||
def __init__(self, korail_id: str, korail_pw: str, auto_login: bool = True, want_feedback: bool = False):
|
||||
import requests
|
||||
|
||||
self._session = requests.session()
|
||||
self._session.headers.update({"User-Agent": DEFAULT_USER_AGENT})
|
||||
self._engine = DynaPathMasterEngine()
|
||||
|
|
@ -187,6 +271,7 @@ class PatchedKorail(Korail):
|
|||
self.login(korail_id, korail_pw)
|
||||
|
||||
def _generate_sid(self, timestamp_ms: int) -> str:
|
||||
ensure_runtime_dependencies()
|
||||
plaintext = f"{self._device}{timestamp_ms}".encode("utf-8")
|
||||
cipher = AES.new(self._sid_key, AES.MODE_CBC, iv=self._sid_key)
|
||||
return base64.b64encode(cipher.encrypt(pad(plaintext, 16))).decode("utf-8") + "\n"
|
||||
|
|
@ -535,6 +620,7 @@ def print_json(payload: dict[str, object]) -> None:
|
|||
|
||||
|
||||
def build_client() -> PatchedKorail:
|
||||
ensure_runtime_dependencies()
|
||||
korail_id = os.environ.get("KSKILL_KTX_ID")
|
||||
korail_pw = os.environ.get("KSKILL_KTX_PASSWORD")
|
||||
if not korail_id or not korail_pw:
|
||||
|
|
|
|||
|
|
@ -216,7 +216,11 @@ test("ktx-booking helper python regression tests pass", () => {
|
|||
const result = childProcess.spawnSync(
|
||||
"python3",
|
||||
["-m", "unittest", "discover", "-s", "scripts", "-p", "test_ktx_booking.py"],
|
||||
{ cwd: repoRoot, encoding: "utf8" },
|
||||
{
|
||||
cwd: repoRoot,
|
||||
encoding: "utf8",
|
||||
env: { ...process.env, PYTHONNOUSERSITE: "1" },
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue