pykorail 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. pykorail-0.1.0/.agents/README.md +72 -0
  2. pykorail-0.1.0/.gitignore +30 -0
  3. pykorail-0.1.0/LICENSE +21 -0
  4. pykorail-0.1.0/PKG-INFO +291 -0
  5. pykorail-0.1.0/README.md +260 -0
  6. pykorail-0.1.0/docs/reference.md +386 -0
  7. pykorail-0.1.0/pyproject.toml +139 -0
  8. pykorail-0.1.0/src/pykorail/__init__.py +124 -0
  9. pykorail-0.1.0/src/pykorail/_compat.py +19 -0
  10. pykorail-0.1.0/src/pykorail/_version.py +24 -0
  11. pykorail-0.1.0/src/pykorail/api.py +112 -0
  12. pykorail-0.1.0/src/pykorail/auth/__init__.py +9 -0
  13. pykorail-0.1.0/src/pykorail/auth/dynapath.py +179 -0
  14. pykorail-0.1.0/src/pykorail/auth/netfunnel.py +140 -0
  15. pykorail-0.1.0/src/pykorail/auth/signer.py +51 -0
  16. pykorail-0.1.0/src/pykorail/client.py +232 -0
  17. pykorail-0.1.0/src/pykorail/constants.py +75 -0
  18. pykorail-0.1.0/src/pykorail/crypto.py +34 -0
  19. pykorail-0.1.0/src/pykorail/device/__init__.py +34 -0
  20. pykorail-0.1.0/src/pykorail/device/catalog.py +145 -0
  21. pykorail-0.1.0/src/pykorail/device/profile.py +61 -0
  22. pykorail-0.1.0/src/pykorail/exceptions/__init__.py +42 -0
  23. pykorail-0.1.0/src/pykorail/exceptions/api.py +63 -0
  24. pykorail-0.1.0/src/pykorail/exceptions/base.py +38 -0
  25. pykorail-0.1.0/src/pykorail/exceptions/network.py +24 -0
  26. pykorail-0.1.0/src/pykorail/exceptions/validation.py +64 -0
  27. pykorail-0.1.0/src/pykorail/models/__init__.py +47 -0
  28. pykorail-0.1.0/src/pykorail/models/card.py +63 -0
  29. pykorail-0.1.0/src/pykorail/models/parsing.py +51 -0
  30. pykorail-0.1.0/src/pykorail/models/passenger.py +142 -0
  31. pykorail-0.1.0/src/pykorail/models/reservation.py +83 -0
  32. pykorail-0.1.0/src/pykorail/models/schedule.py +175 -0
  33. pykorail-0.1.0/src/pykorail/models/seat.py +46 -0
  34. pykorail-0.1.0/src/pykorail/models/station.py +84 -0
  35. pykorail-0.1.0/src/pykorail/models/ticket.py +79 -0
  36. pykorail-0.1.0/src/pykorail/options.py +46 -0
  37. pykorail-0.1.0/src/pykorail/py.typed +0 -0
  38. pykorail-0.1.0/src/pykorail/resources/__init__.py +27 -0
  39. pykorail-0.1.0/src/pykorail/resources/base.py +19 -0
  40. pykorail-0.1.0/src/pykorail/resources/reservations.py +227 -0
  41. pykorail-0.1.0/src/pykorail/resources/stations.py +60 -0
  42. pykorail-0.1.0/src/pykorail/resources/tickets.py +86 -0
  43. pykorail-0.1.0/src/pykorail/resources/trains.py +156 -0
  44. pykorail-0.1.0/src/pykorail/transport.py +111 -0
  45. pykorail-0.1.0/tests/__init__.py +0 -0
  46. pykorail-0.1.0/tests/conftest.py +92 -0
  47. pykorail-0.1.0/tests/payloads.py +133 -0
  48. pykorail-0.1.0/tests/test_card.py +133 -0
  49. pykorail-0.1.0/tests/test_client.py +300 -0
  50. pykorail-0.1.0/tests/test_device.py +165 -0
  51. pykorail-0.1.0/tests/test_dynapath.py +136 -0
  52. pykorail-0.1.0/tests/test_exceptions.py +118 -0
  53. pykorail-0.1.0/tests/test_models.py +432 -0
  54. pykorail-0.1.0/tests/test_netfunnel.py +224 -0
  55. pykorail-0.1.0/tests/test_packaging.py +148 -0
  56. pykorail-0.1.0/tests/test_passenger.py +167 -0
  57. pykorail-0.1.0/tests/test_readme.py +252 -0
  58. pykorail-0.1.0/tests/test_resources.py +651 -0
  59. pykorail-0.1.0/tests/test_style.py +178 -0
  60. pykorail-0.1.0/tests/test_transport.py +123 -0
@@ -0,0 +1,72 @@
1
+ # 에이전트 설정 지도
2
+
3
+ 이 저장소는 다섯 개 코딩 에이전트를 지원합니다. **규범은 루트의 `AGENTS.md` 하나**
4
+ 이고, 각 도구는 자기 진입점에서 거기로 수렴합니다. 규칙을 바꿀 때는 `AGENTS.md` 를
5
+ 고치세요 — 아래 파일들은 도구별 배선일 뿐입니다.
6
+
7
+ | 도구 | 읽는 것 | 비고 |
8
+ | --- | --- | --- |
9
+ | **Codex** | `AGENTS.md` | 루트 파일을 그대로 읽습니다. 추가 설정 없음. |
10
+ | **Claude Code** | `CLAUDE.md` → `@AGENTS.md` | `.claude/agents/` 서브에이전트, `.claude/skills/` 스킬 |
11
+ | **Pi** | `AGENTS.md` | `.agents/skills` → `.claude/skills` 심볼릭 링크로 스킬 공유 |
12
+ | **Cursor** | `.cursor/rules/*.mdc` | 글롭 범위별 규칙 3개 (아래 참조) |
13
+ | **OpenCode** | `AGENTS.md` + `.opencode/agents/` | 서브에이전트는 OpenCode 프론트매터 스키마 |
14
+
15
+ ## 파일 배치
16
+
17
+ ```
18
+ AGENTS.md ← 규범 (Codex · Pi · OpenCode 가 직접 읽음)
19
+ CLAUDE.md ← @AGENTS.md 임포트 + Claude 전용 안내
20
+
21
+ .claude/
22
+ ├── agents/ ← Claude Code 서브에이전트
23
+ │ ├── architecture-guard.md
24
+ │ ├── wire-parity-auditor.md
25
+ │ └── test-author.md
26
+ └── skills/ ← 스킬 원본 (Claude Code + Pi 공용)
27
+ ├── verify/SKILL.md
28
+ ├── add-endpoint/SKILL.md
29
+ └── add-error-code/SKILL.md
30
+
31
+ .agents/
32
+ ├── README.md ← 이 파일
33
+ └── skills → ../.claude/skills ← 심볼릭 링크 (Pi 가 .agents/skills 를 읽음)
34
+
35
+ .opencode/agents/ ← 같은 서브에이전트, OpenCode 스키마
36
+ ├── architecture-guard.md
37
+ ├── wire-parity-auditor.md
38
+ └── test-author.md
39
+
40
+ .cursor/rules/
41
+ ├── architecture.mdc ← alwaysApply: true
42
+ ├── python-style.mdc ← globs: src/**/*.py
43
+ └── testing.mdc ← globs: tests/**/*.py
44
+ ```
45
+
46
+ ## 서브에이전트
47
+
48
+ 세 개 모두 Claude Code(`.claude/agents/`)와 OpenCode(`.opencode/agents/`)에 같은
49
+ 내용으로 존재합니다. 본문은 동일하고 프론트매터 스키마만 다릅니다.
50
+
51
+ - **architecture-guard** (읽기 전용) — 계층 위반, 클라이언트에 붙은 엔드포인트,
52
+ 가변 모델, 잘못된 상속. `resources/`·`models/`·`client.py`·`api.py` 수정 후.
53
+ - **wire-parity-auditor** (읽기 전용) — 요청 페이로드가 그대로인지 증명.
54
+ `constants.py`·`auth/`·`crypto.py`·폼 필드를 건드렸다면 **반드시**.
55
+ - **test-author** (쓰기 가능) — 규약에 맞는 pytest 작성. 커버리지가 90% 아래로
56
+ 떨어졌을 때.
57
+
58
+ ## 스킬
59
+
60
+ - **verify** — 전체 게이트 실행 + 실패 진단
61
+ - **add-endpoint** — 새 코레일 엔드포인트를 상수→모델→리소스→테스트로 붙이는 절차
62
+ - **add-error-code** — 새 응답 코드를 예외 계층에 매핑
63
+
64
+ ## 유지보수
65
+
66
+ - **규칙을 바꾸려면 `AGENTS.md`.** 다섯 도구가 전부 따라옵니다 (Cursor 는
67
+ `.cursor/rules/` 도 함께 손봐야 합니다 — 자체 포맷이라 임포트가 안 됩니다).
68
+ - **서브에이전트를 바꾸면 `.claude/agents/` 와 `.opencode/agents/` 를 함께**
69
+ 고치세요. 본문은 같아야 합니다.
70
+ - **스킬은 `.claude/skills/` 한 곳만** 고치면 됩니다 — Pi 는 심볼릭 링크로 봅니다.
71
+ 심볼릭 링크를 지원하지 않는 환경(일부 Windows 체크아웃)에서는 Pi 의 스킬 탐색만
72
+ 조용히 비고, `AGENTS.md` 는 그대로 동작합니다.
@@ -0,0 +1,30 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ venv/
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ .ty_cache/
11
+ .DS_Store
12
+
13
+ # 커버리지 산출물
14
+ .coverage
15
+ .coverage.*
16
+ coverage.xml
17
+ junit.xml
18
+ htmlcov/
19
+
20
+ # CI 가 uv.lock 에서 생성하는 것들 (커밋 대상 아님)
21
+ requirements.txt
22
+ *.sarif
23
+
24
+ # hatch-vcs 가 빌드 시 생성
25
+ src/pykorail/_version.py
26
+
27
+ # 로컬 실험용 스크립트 — 자격증명이 들어가기 쉬우므로 절대 커밋하지 않습니다.
28
+ /a.py
29
+ /scratch*.py
30
+ /local/
pykorail-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 leegyurak
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,291 @@
1
+ Metadata-Version: 2.5
2
+ Name: pykorail
3
+ Version: 0.1.0
4
+ Summary: 코레일(KTX) 스마트 예매 비공식 Python 클라이언트
5
+ Project-URL: Homepage, https://github.com/leegyurak/pykorail
6
+ Project-URL: Repository, https://github.com/leegyurak/pykorail
7
+ Project-URL: Issues, https://github.com/leegyurak/pykorail/issues
8
+ Author-email: leegyurak <devgyurak@gmail.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: korail,korea,ktx,letskorail,reservation,train
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Natural Language :: Korean
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.10
25
+ Requires-Dist: certifi>=2024.2.2
26
+ Requires-Dist: curl-cffi>=0.7.0
27
+ Requires-Dist: pycryptodome>=3.20.0
28
+ Provides-Extra: fallback
29
+ Requires-Dist: requests>=2.31.0; extra == 'fallback'
30
+ Description-Content-Type: text/markdown
31
+
32
+ <div align="center">
33
+
34
+ # 🚄 pykorail
35
+
36
+ **파이썬으로 KTX 표를 조회하고 예매합니다**
37
+
38
+ 코레일 스마트 예매(코레일톡) API 를 감싼 비공식 클라이언트
39
+
40
+ [![CI](https://github.com/leegyurak/pykorail/actions/workflows/ci.yml/badge.svg)](https://github.com/leegyurak/pykorail/actions/workflows/ci.yml)
41
+ [![PyPI](https://img.shields.io/pypi/v/pykorail?color=3775A9&logo=pypi&logoColor=white)](https://pypi.org/project/pykorail/)
42
+ [![Python](https://img.shields.io/pypi/pyversions/pykorail?color=3776AB&logo=python&logoColor=white)](https://pypi.org/project/pykorail/)
43
+ [![License](https://img.shields.io/pypi/l/pykorail?color=green)](LICENSE)
44
+
45
+ [빠른 시작](#빠른-시작) · [할 수 있는 것](#할-수-있는-것) · [API 레퍼런스](docs/reference.md) · [기여하기](CONTRIBUTING.md)
46
+
47
+ </div>
48
+
49
+ ---
50
+
51
+ ## 빠른 시작
52
+
53
+ ```bash
54
+ pip install pykorail
55
+ ```
56
+
57
+ ```python
58
+ from datetime import datetime
59
+ from pykorail import Korail
60
+
61
+ with Korail.logged_in("me@example.com", "password") as korail:
62
+ trains = korail.trains.search("서울", "부산", depart_after=datetime(2026, 4, 1, 9, 0))
63
+ for train in trains:
64
+ print(train)
65
+ ```
66
+
67
+ ```text
68
+ [KTX 101] 04/01 09:00~12:30 서울~부산 특실 가능, 일반실 가능 (3시간 30분)
69
+ [KTX 103] 04/01 10:00~13:30 서울~부산 특실 매진, 일반실 가능 (3시간 30분)
70
+ [KTX 105] 04/01 11:00~14:35 서울~부산 특실 가능, 일반실 매진 (3시간 35분)
71
+ ```
72
+
73
+ > [!WARNING]
74
+ > 코레일과 아무 관련 없는 **비공식** 라이브러리입니다. 문서화되지 않은 앱 API 를 쓰기
75
+ > 때문에 코레일이 앱을 바꾸면 예고 없이 멈출 수 있습니다. 이용 약관과 관련 법령을
76
+ > 지키는 것은 사용자 책임이며, 과도한 자동 요청은 계정 제재로 이어질 수 있습니다.
77
+
78
+ ---
79
+
80
+ ## 할 수 있는 것
81
+
82
+ ### 표 예매하고 결제하기
83
+
84
+ ```python
85
+ from pykorail import AdultPassenger, Card, ChildPassenger
86
+
87
+ trains = korail.trains.search(
88
+ "서울",
89
+ "부산",
90
+ depart_after=datetime(2026, 4, 1, 9, 0),
91
+ passengers=[AdultPassenger(2), ChildPassenger(1)],
92
+ )
93
+
94
+ reservation = korail.reservations.create(trains[0])
95
+ print(reservation)
96
+ # [KTX 101] … 119600원(3석), 구입기한 4월 1일 09:20
97
+
98
+ korail.reservations.pay(
99
+ reservation,
100
+ Card(number="1234567812345678", password="12", verify_number="900101", expire="2812"),
101
+ )
102
+ ```
103
+
104
+ <details>
105
+ <summary><b>취소표 기다리기</b></summary>
106
+
107
+ ```python
108
+ import time
109
+ from pykorail import NoResultsError, PastDepartureError
110
+
111
+ departure = datetime(2026, 4, 1, 9, 0)
112
+
113
+ while True:
114
+ try:
115
+ trains = korail.trains.search("서울", "부산", depart_after=departure)
116
+ except NoResultsError:
117
+ time.sleep(30) # 서버에 부담 주지 않게 넉넉히 쉬세요
118
+ continue
119
+ except PastDepartureError:
120
+ break # 열차가 이미 떠났습니다 — 무한정 돌지 않도록
121
+
122
+ korail.reservations.create(trains[0])
123
+ break
124
+ ```
125
+
126
+ </details>
127
+
128
+ <details>
129
+ <summary><b>매진일 때 예약대기 걸기</b></summary>
130
+
131
+ ```python
132
+ trains = korail.trains.search("서울", "부산", include_waiting_list=True)
133
+ waitable = [t for t in trains if not t.has_seat() and t.has_waiting_list()]
134
+
135
+ reservation = korail.reservations.create(waitable[0])
136
+ assert reservation.is_waiting
137
+ ```
138
+
139
+ </details>
140
+
141
+ <details>
142
+ <summary><b>내 예약·승차권 보기</b></summary>
143
+
144
+ ```python
145
+ for reservation in korail.reservations.all():
146
+ print(reservation.train.dep_name, "→", reservation.train.arr_name, reservation.price)
147
+
148
+ for ticket in korail.tickets.all():
149
+ print(ticket.ticket_no, ticket.car_no, ticket.seat_no)
150
+ korail.tickets.refund(ticket) # 환불
151
+ ```
152
+
153
+ </details>
154
+
155
+ <details>
156
+ <summary><b>기기 프로파일 고정하기</b> — 여러 번 실행한다면</summary>
157
+
158
+ 실행할 때마다 다른 기기인 척하면 오히려 부자연스럽습니다. 한 번 뽑아 `id` 를
159
+ 저장해 두고 계속 쓰세요.
160
+
161
+ ```python
162
+ from pykorail.device import profile_by_id, random_profile
163
+
164
+ profile = profile_by_id(saved_id) or random_profile()
165
+ korail = Korail(device_profile=profile)
166
+ ```
167
+
168
+ </details>
169
+
170
+ ---
171
+
172
+ ## 알아두면 좋은 것
173
+
174
+
175
+ | | |
176
+ | -------------- | ---------------------------------------------------- |
177
+ | **역은 이름으로** | `"서울역"` 이 아니라 `"서울"`. 오타면 요청 전에 막고 비슷한 역을 알려줍니다. |
178
+ | **시각은 항상 KST** | `datetime(2026, 4, 1, 9)` 는 서버가 어느 타임존이든 한국시간 오전 9시. |
179
+ | **지난 시각은 거부** | 서버가 과거에도 빈 결과만 줘서 "떠난 열차"와 "열차 없음"이 구분되지 않습니다. |
180
+ | **실패는 예외로** | `cancel()` · `pay()` 는 성공 시 아무것도 반환하지 않습니다. |
181
+
182
+
183
+ ```python
184
+ from pykorail import KorailError, SoldOutError
185
+
186
+ try:
187
+ korail.reservations.create(train)
188
+ except SoldOutError:
189
+ ... # 매진 — 다음 열차로
190
+ except KorailError as exc:
191
+ print(exc.msg, exc.code) # 코레일이 준 메시지와 코드
192
+ ```
193
+
194
+ ---
195
+
196
+ ## 문서
197
+
198
+
199
+ | | |
200
+ | ----------------------------------------------------- | -------------------------------------------- |
201
+ | 📖 [**API 레퍼런스**](docs/reference.md) | 전체 메서드 · 모델 · 옵션 · 예외 |
202
+ | 🤝 [기여 가이드](CONTRIBUTING.md) | 개발 환경, 테스트 규칙, PR 절차 |
203
+ | 🏛 [아키텍처 규약](AGENTS.md) | 계층 구조, 외부 API 불변식 |
204
+ | 🤖 [에이전트 설정](.agents/README.md) | Codex · Claude Code · Pi · Cursor · OpenCode |
205
+ | 🔐 [보안 정책](SECURITY.md) · [행동 강령](CODE_OF_CONDUCT.md) | |
206
+
207
+
208
+ ---
209
+
210
+ ## 자주 묻는 것
211
+
212
+ <details>
213
+ <summary><b>로그인이 안 됩니다</b></summary>
214
+
215
+ 설치할 때 경고가 떴다면 `curl_cffi` 대신 `requests` 로 돌고 있을 수 있습니다.
216
+ 코레일은 TLS 지문을 보기 때문에 이때 로그인이 거부될 수 있습니다.
217
+ `pip install curl_cffi` 로 해결되는 경우가 대부분입니다.
218
+
219
+ 휴대폰 번호로 로그인한다면 **하이픈을 넣어야 합니다** (`010-1234-5678`).
220
+ 빠뜨리면 회원번호로 조회돼 엉뚱하게 실패합니다.
221
+
222
+ </details>
223
+
224
+ <details>
225
+ <summary><b>Windows 에서 설치가 안 됩니다</b></summary>
226
+
227
+ `curl_cffi` 의 libcurl DLL 로드가 실패하는 환경이 있습니다.
228
+ `pip install "pykorail[fallback]"` 로 requests 폴백을 함께 설치하세요.
229
+ 다만 위에 적은 이유로 로그인이 막힐 수 있습니다.
230
+
231
+ </details>
232
+
233
+ <details>
234
+ <summary><b>SRT 도 되나요?</b></summary>
235
+
236
+ 아니요. SRT(수서고속철도)는 다른 회사의 다른 시스템입니다.
237
+
238
+ </details>
239
+
240
+ <details>
241
+ <summary><b>어제까지 되던 게 오늘 안 됩니다</b></summary>
242
+
243
+ 코레일이 서버를 바꿨을 수 있습니다.
244
+ [API 변경 이슈](https://github.com/leegyurak/pykorail/issues/new?template=external_api_change.yml)
245
+ 로 알려주시면 대응하겠습니다. **가장 도움이 되는 기여입니다.**
246
+
247
+ </details>
248
+
249
+ <details>
250
+ <summary><b>예매가 확실히 되나요?</b></summary>
251
+
252
+ 이 라이브러리는 앱과 같은 요청을 보낼 뿐이고, 좌석 배정은 코레일 서버가 합니다.
253
+ 명절 예매처럼 경쟁이 심한 상황에서 성공을 보장하지 않습니다.
254
+
255
+ </details>
256
+
257
+ ---
258
+
259
+ ## 개발
260
+
261
+ 파이썬 3.10 이상 (3.10 – 3.14 에서 테스트합니다).
262
+
263
+ ```bash
264
+ uv sync --all-extras
265
+ uv run ruff format && uv run ruff check && uv run ty check && uv run pytest
266
+ ```
267
+
268
+ 네 개를 전부 통과해야 합니다. 커버리지 하한 90%, 현재 96%.
269
+
270
+ 기여를 환영합니다 — [`CONTRIBUTING.md`](CONTRIBUTING.md) 부터 보세요. 새 엔드포인트는
271
+ **공식 코레일톡+ APK 를 디컴파일해 경로와 폼 필드를 확인한 뒤에만** 추가합니다.
272
+
273
+ ### 릴리스
274
+
275
+ 버전의 유일한 출처는 **git 태그**입니다. 파일을 손으로 고칠 필요가 없습니다.
276
+
277
+ ```bash
278
+ git tag v0.2.0 && git push origin v0.2.0
279
+ ```
280
+
281
+ 태그를 밀면 전 버전 게이트 → 보안 스캔 → 빌드 → PyPI 업로드 → 릴리스 노트 생성이
282
+ 자동으로 돕니다.
283
+
284
+ ---
285
+
286
+ <div align="center">
287
+
288
+ <sub>MIT License · 코레일과 무관한 비공식 프로젝트</sub>
289
+
290
+ </div>
291
+
@@ -0,0 +1,260 @@
1
+ <div align="center">
2
+
3
+ # 🚄 pykorail
4
+
5
+ **파이썬으로 KTX 표를 조회하고 예매합니다**
6
+
7
+ 코레일 스마트 예매(코레일톡) API 를 감싼 비공식 클라이언트
8
+
9
+ [![CI](https://github.com/leegyurak/pykorail/actions/workflows/ci.yml/badge.svg)](https://github.com/leegyurak/pykorail/actions/workflows/ci.yml)
10
+ [![PyPI](https://img.shields.io/pypi/v/pykorail?color=3775A9&logo=pypi&logoColor=white)](https://pypi.org/project/pykorail/)
11
+ [![Python](https://img.shields.io/pypi/pyversions/pykorail?color=3776AB&logo=python&logoColor=white)](https://pypi.org/project/pykorail/)
12
+ [![License](https://img.shields.io/pypi/l/pykorail?color=green)](LICENSE)
13
+
14
+ [빠른 시작](#빠른-시작) · [할 수 있는 것](#할-수-있는-것) · [API 레퍼런스](docs/reference.md) · [기여하기](CONTRIBUTING.md)
15
+
16
+ </div>
17
+
18
+ ---
19
+
20
+ ## 빠른 시작
21
+
22
+ ```bash
23
+ pip install pykorail
24
+ ```
25
+
26
+ ```python
27
+ from datetime import datetime
28
+ from pykorail import Korail
29
+
30
+ with Korail.logged_in("me@example.com", "password") as korail:
31
+ trains = korail.trains.search("서울", "부산", depart_after=datetime(2026, 4, 1, 9, 0))
32
+ for train in trains:
33
+ print(train)
34
+ ```
35
+
36
+ ```text
37
+ [KTX 101] 04/01 09:00~12:30 서울~부산 특실 가능, 일반실 가능 (3시간 30분)
38
+ [KTX 103] 04/01 10:00~13:30 서울~부산 특실 매진, 일반실 가능 (3시간 30분)
39
+ [KTX 105] 04/01 11:00~14:35 서울~부산 특실 가능, 일반실 매진 (3시간 35분)
40
+ ```
41
+
42
+ > [!WARNING]
43
+ > 코레일과 아무 관련 없는 **비공식** 라이브러리입니다. 문서화되지 않은 앱 API 를 쓰기
44
+ > 때문에 코레일이 앱을 바꾸면 예고 없이 멈출 수 있습니다. 이용 약관과 관련 법령을
45
+ > 지키는 것은 사용자 책임이며, 과도한 자동 요청은 계정 제재로 이어질 수 있습니다.
46
+
47
+ ---
48
+
49
+ ## 할 수 있는 것
50
+
51
+ ### 표 예매하고 결제하기
52
+
53
+ ```python
54
+ from pykorail import AdultPassenger, Card, ChildPassenger
55
+
56
+ trains = korail.trains.search(
57
+ "서울",
58
+ "부산",
59
+ depart_after=datetime(2026, 4, 1, 9, 0),
60
+ passengers=[AdultPassenger(2), ChildPassenger(1)],
61
+ )
62
+
63
+ reservation = korail.reservations.create(trains[0])
64
+ print(reservation)
65
+ # [KTX 101] … 119600원(3석), 구입기한 4월 1일 09:20
66
+
67
+ korail.reservations.pay(
68
+ reservation,
69
+ Card(number="1234567812345678", password="12", verify_number="900101", expire="2812"),
70
+ )
71
+ ```
72
+
73
+ <details>
74
+ <summary><b>취소표 기다리기</b></summary>
75
+
76
+ ```python
77
+ import time
78
+ from pykorail import NoResultsError, PastDepartureError
79
+
80
+ departure = datetime(2026, 4, 1, 9, 0)
81
+
82
+ while True:
83
+ try:
84
+ trains = korail.trains.search("서울", "부산", depart_after=departure)
85
+ except NoResultsError:
86
+ time.sleep(30) # 서버에 부담 주지 않게 넉넉히 쉬세요
87
+ continue
88
+ except PastDepartureError:
89
+ break # 열차가 이미 떠났습니다 — 무한정 돌지 않도록
90
+
91
+ korail.reservations.create(trains[0])
92
+ break
93
+ ```
94
+
95
+ </details>
96
+
97
+ <details>
98
+ <summary><b>매진일 때 예약대기 걸기</b></summary>
99
+
100
+ ```python
101
+ trains = korail.trains.search("서울", "부산", include_waiting_list=True)
102
+ waitable = [t for t in trains if not t.has_seat() and t.has_waiting_list()]
103
+
104
+ reservation = korail.reservations.create(waitable[0])
105
+ assert reservation.is_waiting
106
+ ```
107
+
108
+ </details>
109
+
110
+ <details>
111
+ <summary><b>내 예약·승차권 보기</b></summary>
112
+
113
+ ```python
114
+ for reservation in korail.reservations.all():
115
+ print(reservation.train.dep_name, "→", reservation.train.arr_name, reservation.price)
116
+
117
+ for ticket in korail.tickets.all():
118
+ print(ticket.ticket_no, ticket.car_no, ticket.seat_no)
119
+ korail.tickets.refund(ticket) # 환불
120
+ ```
121
+
122
+ </details>
123
+
124
+ <details>
125
+ <summary><b>기기 프로파일 고정하기</b> — 여러 번 실행한다면</summary>
126
+
127
+ 실행할 때마다 다른 기기인 척하면 오히려 부자연스럽습니다. 한 번 뽑아 `id` 를
128
+ 저장해 두고 계속 쓰세요.
129
+
130
+ ```python
131
+ from pykorail.device import profile_by_id, random_profile
132
+
133
+ profile = profile_by_id(saved_id) or random_profile()
134
+ korail = Korail(device_profile=profile)
135
+ ```
136
+
137
+ </details>
138
+
139
+ ---
140
+
141
+ ## 알아두면 좋은 것
142
+
143
+
144
+ | | |
145
+ | -------------- | ---------------------------------------------------- |
146
+ | **역은 이름으로** | `"서울역"` 이 아니라 `"서울"`. 오타면 요청 전에 막고 비슷한 역을 알려줍니다. |
147
+ | **시각은 항상 KST** | `datetime(2026, 4, 1, 9)` 는 서버가 어느 타임존이든 한국시간 오전 9시. |
148
+ | **지난 시각은 거부** | 서버가 과거에도 빈 결과만 줘서 "떠난 열차"와 "열차 없음"이 구분되지 않습니다. |
149
+ | **실패는 예외로** | `cancel()` · `pay()` 는 성공 시 아무것도 반환하지 않습니다. |
150
+
151
+
152
+ ```python
153
+ from pykorail import KorailError, SoldOutError
154
+
155
+ try:
156
+ korail.reservations.create(train)
157
+ except SoldOutError:
158
+ ... # 매진 — 다음 열차로
159
+ except KorailError as exc:
160
+ print(exc.msg, exc.code) # 코레일이 준 메시지와 코드
161
+ ```
162
+
163
+ ---
164
+
165
+ ## 문서
166
+
167
+
168
+ | | |
169
+ | ----------------------------------------------------- | -------------------------------------------- |
170
+ | 📖 [**API 레퍼런스**](docs/reference.md) | 전체 메서드 · 모델 · 옵션 · 예외 |
171
+ | 🤝 [기여 가이드](CONTRIBUTING.md) | 개발 환경, 테스트 규칙, PR 절차 |
172
+ | 🏛 [아키텍처 규약](AGENTS.md) | 계층 구조, 외부 API 불변식 |
173
+ | 🤖 [에이전트 설정](.agents/README.md) | Codex · Claude Code · Pi · Cursor · OpenCode |
174
+ | 🔐 [보안 정책](SECURITY.md) · [행동 강령](CODE_OF_CONDUCT.md) | |
175
+
176
+
177
+ ---
178
+
179
+ ## 자주 묻는 것
180
+
181
+ <details>
182
+ <summary><b>로그인이 안 됩니다</b></summary>
183
+
184
+ 설치할 때 경고가 떴다면 `curl_cffi` 대신 `requests` 로 돌고 있을 수 있습니다.
185
+ 코레일은 TLS 지문을 보기 때문에 이때 로그인이 거부될 수 있습니다.
186
+ `pip install curl_cffi` 로 해결되는 경우가 대부분입니다.
187
+
188
+ 휴대폰 번호로 로그인한다면 **하이픈을 넣어야 합니다** (`010-1234-5678`).
189
+ 빠뜨리면 회원번호로 조회돼 엉뚱하게 실패합니다.
190
+
191
+ </details>
192
+
193
+ <details>
194
+ <summary><b>Windows 에서 설치가 안 됩니다</b></summary>
195
+
196
+ `curl_cffi` 의 libcurl DLL 로드가 실패하는 환경이 있습니다.
197
+ `pip install "pykorail[fallback]"` 로 requests 폴백을 함께 설치하세요.
198
+ 다만 위에 적은 이유로 로그인이 막힐 수 있습니다.
199
+
200
+ </details>
201
+
202
+ <details>
203
+ <summary><b>SRT 도 되나요?</b></summary>
204
+
205
+ 아니요. SRT(수서고속철도)는 다른 회사의 다른 시스템입니다.
206
+
207
+ </details>
208
+
209
+ <details>
210
+ <summary><b>어제까지 되던 게 오늘 안 됩니다</b></summary>
211
+
212
+ 코레일이 서버를 바꿨을 수 있습니다.
213
+ [API 변경 이슈](https://github.com/leegyurak/pykorail/issues/new?template=external_api_change.yml)
214
+ 로 알려주시면 대응하겠습니다. **가장 도움이 되는 기여입니다.**
215
+
216
+ </details>
217
+
218
+ <details>
219
+ <summary><b>예매가 확실히 되나요?</b></summary>
220
+
221
+ 이 라이브러리는 앱과 같은 요청을 보낼 뿐이고, 좌석 배정은 코레일 서버가 합니다.
222
+ 명절 예매처럼 경쟁이 심한 상황에서 성공을 보장하지 않습니다.
223
+
224
+ </details>
225
+
226
+ ---
227
+
228
+ ## 개발
229
+
230
+ 파이썬 3.10 이상 (3.10 – 3.14 에서 테스트합니다).
231
+
232
+ ```bash
233
+ uv sync --all-extras
234
+ uv run ruff format && uv run ruff check && uv run ty check && uv run pytest
235
+ ```
236
+
237
+ 네 개를 전부 통과해야 합니다. 커버리지 하한 90%, 현재 96%.
238
+
239
+ 기여를 환영합니다 — [`CONTRIBUTING.md`](CONTRIBUTING.md) 부터 보세요. 새 엔드포인트는
240
+ **공식 코레일톡+ APK 를 디컴파일해 경로와 폼 필드를 확인한 뒤에만** 추가합니다.
241
+
242
+ ### 릴리스
243
+
244
+ 버전의 유일한 출처는 **git 태그**입니다. 파일을 손으로 고칠 필요가 없습니다.
245
+
246
+ ```bash
247
+ git tag v0.2.0 && git push origin v0.2.0
248
+ ```
249
+
250
+ 태그를 밀면 전 버전 게이트 → 보안 스캔 → 빌드 → PyPI 업로드 → 릴리스 노트 생성이
251
+ 자동으로 돕니다.
252
+
253
+ ---
254
+
255
+ <div align="center">
256
+
257
+ <sub>MIT License · 코레일과 무관한 비공식 프로젝트</sub>
258
+
259
+ </div>
260
+