kiwoom-cli 0.1.0__py3-none-any.whl

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.
kiwoom_cli/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Kiwoom Securities REST API CLI."""
2
+
3
+ __version__ = "0.1.0"
kiwoom_cli/api_spec.py ADDED
@@ -0,0 +1,229 @@
1
+ """API specification registry.
2
+
3
+ Maps API IDs to their URL paths. All APIs use POST and share URLs
4
+ differentiated by the `api-id` header.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ # API ID -> (url_path, description_ko)
10
+ API_REGISTRY: dict[str, tuple[str, str]] = {
11
+ # === OAuth ===
12
+ "au10001": ("/oauth2/token", "접근토큰 발급"),
13
+ "au10002": ("/oauth2/revoke", "접근토큰폐기"),
14
+ # === 계좌 (Account) ===
15
+ "ka00001": ("/api/dostk/acnt", "계좌번호조회"),
16
+ "ka01690": ("/api/dostk/acnt", "일별잔고수익률"),
17
+ "ka10072": ("/api/dostk/acnt", "일자별종목별실현손익요청_일자"),
18
+ "ka10073": ("/api/dostk/acnt", "일자별종목별실현손익요청_기간"),
19
+ "ka10074": ("/api/dostk/acnt", "일자별실현손익요청"),
20
+ "ka10075": ("/api/dostk/acnt", "미체결요청"),
21
+ "ka10076": ("/api/dostk/acnt", "체결요청"),
22
+ "ka10077": ("/api/dostk/acnt", "당일실현손익상세요청"),
23
+ "ka10085": ("/api/dostk/acnt", "계좌수익률요청"),
24
+ "ka10088": ("/api/dostk/acnt", "미체결 분할주문 상세"),
25
+ "ka10170": ("/api/dostk/acnt", "당일매매일지요청"),
26
+ "kt00001": ("/api/dostk/acnt", "예수금상세현황요청"),
27
+ "kt00002": ("/api/dostk/acnt", "일별추정예탁자산현황요청"),
28
+ "kt00003": ("/api/dostk/acnt", "추정자산조회요청"),
29
+ "kt00004": ("/api/dostk/acnt", "계좌평가현황요청"),
30
+ "kt00005": ("/api/dostk/acnt", "체결잔고요청"),
31
+ "kt00007": ("/api/dostk/acnt", "계좌별주문체결내역상세요청"),
32
+ "kt00008": ("/api/dostk/acnt", "계좌별익일결제예정내역요청"),
33
+ "kt00009": ("/api/dostk/acnt", "계좌별주문체결현황요청"),
34
+ "kt00010": ("/api/dostk/acnt", "주문인출가능금액요청"),
35
+ "kt00011": ("/api/dostk/acnt", "증거금율별주문가능수량조회요청"),
36
+ "kt00012": ("/api/dostk/acnt", "신용보증금율별주문가능수량조회요청"),
37
+ "kt00013": ("/api/dostk/acnt", "증거금세부내역조회요청"),
38
+ "kt00015": ("/api/dostk/acnt", "위탁종합거래내역요청"),
39
+ "kt00016": ("/api/dostk/acnt", "일별계좌수익률상세현황요청"),
40
+ "kt00017": ("/api/dostk/acnt", "계좌별당일현황요청"),
41
+ "kt00018": ("/api/dostk/acnt", "계좌평가잔고내역요청"),
42
+ "kt50020": ("/api/dostk/acnt", "금현물 잔고확인"),
43
+ "kt50021": ("/api/dostk/acnt", "금현물 예수금"),
44
+ "kt50030": ("/api/dostk/acnt", "금현물 주문체결전체조회"),
45
+ "kt50031": ("/api/dostk/acnt", "금현물 주문체결조회"),
46
+ "kt50032": ("/api/dostk/acnt", "금현물 거래내역조회"),
47
+ "kt50075": ("/api/dostk/acnt", "금현물 미체결조회"),
48
+ # === 종목정보 (Stock Info) ===
49
+ "ka00198": ("/api/dostk/stkinfo", "실시간종목조회순위"),
50
+ "ka10001": ("/api/dostk/stkinfo", "주식기본정보요청"),
51
+ "ka10002": ("/api/dostk/stkinfo", "주식거래원요청"),
52
+ "ka10003": ("/api/dostk/stkinfo", "체결정보요청"),
53
+ "ka10013": ("/api/dostk/stkinfo", "신용매매동향요청"),
54
+ "ka10015": ("/api/dostk/stkinfo", "일별거래상세요청"),
55
+ "ka10016": ("/api/dostk/stkinfo", "신고저가요청"),
56
+ "ka10017": ("/api/dostk/stkinfo", "상하한가요청"),
57
+ "ka10018": ("/api/dostk/stkinfo", "고저가근접요청"),
58
+ "ka10019": ("/api/dostk/stkinfo", "가격급등락요청"),
59
+ "ka10024": ("/api/dostk/stkinfo", "거래량갱신요청"),
60
+ "ka10025": ("/api/dostk/stkinfo", "매물대집중요청"),
61
+ "ka10026": ("/api/dostk/stkinfo", "고저PER요청"),
62
+ "ka10028": ("/api/dostk/stkinfo", "시가대비등락률요청"),
63
+ "ka10043": ("/api/dostk/stkinfo", "거래원매물대분석요청"),
64
+ "ka10052": ("/api/dostk/stkinfo", "거래원순간거래량요청"),
65
+ "ka10054": ("/api/dostk/stkinfo", "변동성완화장치발동종목요청"),
66
+ "ka10055": ("/api/dostk/stkinfo", "당일전일체결량요청"),
67
+ "ka10058": ("/api/dostk/stkinfo", "투자자별일별매매종목요청"),
68
+ "ka10059": ("/api/dostk/stkinfo", "종목별투자자기관별요청"),
69
+ "ka10061": ("/api/dostk/stkinfo", "종목별투자자기관별합계요청"),
70
+ "ka10084": ("/api/dostk/stkinfo", "당일전일체결요청"),
71
+ "ka10095": ("/api/dostk/stkinfo", "관심종목정보요청"),
72
+ "ka10099": ("/api/dostk/stkinfo", "종목정보 리스트"),
73
+ "ka10100": ("/api/dostk/stkinfo", "종목정보 조회"),
74
+ "ka10101": ("/api/dostk/stkinfo", "업종코드 리스트"),
75
+ "ka10102": ("/api/dostk/stkinfo", "회원사 리스트"),
76
+ "ka90003": ("/api/dostk/stkinfo", "프로그램순매수상위50요청"),
77
+ "ka90004": ("/api/dostk/stkinfo", "종목별프로그램매매현황요청"),
78
+ "kt20016": ("/api/dostk/stkinfo", "신용융자 가능종목요청"),
79
+ "kt20017": ("/api/dostk/stkinfo", "신용융자 가능문의"),
80
+ # === 시세 (Market Conditions) ===
81
+ "ka10004": ("/api/dostk/mrkcond", "주식호가요청"),
82
+ "ka10005": ("/api/dostk/mrkcond", "주식일주월시분요청"),
83
+ "ka10006": ("/api/dostk/mrkcond", "주식시분요청"),
84
+ "ka10007": ("/api/dostk/mrkcond", "시세표성정보요청"),
85
+ "ka10011": ("/api/dostk/mrkcond", "신주인수권전체시세요청"),
86
+ "ka10044": ("/api/dostk/mrkcond", "일별기관매매종목요청"),
87
+ "ka10045": ("/api/dostk/mrkcond", "종목별기관매매추이요청"),
88
+ "ka10046": ("/api/dostk/mrkcond", "체결강도추이시간별요청"),
89
+ "ka10047": ("/api/dostk/mrkcond", "체결강도추이일별요청"),
90
+ "ka10063": ("/api/dostk/mrkcond", "장중투자자별매매요청"),
91
+ "ka10066": ("/api/dostk/mrkcond", "장마감후투자자별매매요청"),
92
+ "ka10078": ("/api/dostk/mrkcond", "증권사별종목매매동향요청"),
93
+ "ka10086": ("/api/dostk/mrkcond", "일별주가요청"),
94
+ "ka10087": ("/api/dostk/mrkcond", "시간외단일가요청"),
95
+ "ka50010": ("/api/dostk/mrkcond", "금현물체결추이"),
96
+ "ka50012": ("/api/dostk/mrkcond", "금현물일별추이"),
97
+ "ka50087": ("/api/dostk/mrkcond", "금현물예상체결"),
98
+ "ka50100": ("/api/dostk/mrkcond", "금현물 시세정보"),
99
+ "ka50101": ("/api/dostk/mrkcond", "금현물 호가"),
100
+ "ka90005": ("/api/dostk/mrkcond", "프로그램매매추이요청 시간대별"),
101
+ "ka90006": ("/api/dostk/mrkcond", "프로그램매매차익잔고추이요청"),
102
+ "ka90007": ("/api/dostk/mrkcond", "프로그램매매누적추이요청"),
103
+ "ka90008": ("/api/dostk/mrkcond", "종목시간별프로그램매매추이요청"),
104
+ "ka90010": ("/api/dostk/mrkcond", "프로그램매매추이요청 일자별"),
105
+ "ka90013": ("/api/dostk/mrkcond", "종목일별프로그램매매추이요청"),
106
+ # === 주문 (Orders) ===
107
+ "kt10000": ("/api/dostk/ordr", "주식 매수주문"),
108
+ "kt10001": ("/api/dostk/ordr", "주식 매도주문"),
109
+ "kt10002": ("/api/dostk/ordr", "주식 정정주문"),
110
+ "kt10003": ("/api/dostk/ordr", "주식 취소주문"),
111
+ "kt50000": ("/api/dostk/ordr", "금현물 매수주문"),
112
+ "kt50001": ("/api/dostk/ordr", "금현물 매도주문"),
113
+ "kt50002": ("/api/dostk/ordr", "금현물 정정주문"),
114
+ "kt50003": ("/api/dostk/ordr", "금현물 취소주문"),
115
+ # === 신용주문 (Credit Orders) ===
116
+ "kt10006": ("/api/dostk/crdordr", "신용 매수주문"),
117
+ "kt10007": ("/api/dostk/crdordr", "신용 매도주문"),
118
+ "kt10008": ("/api/dostk/crdordr", "신용 정정주문"),
119
+ "kt10009": ("/api/dostk/crdordr", "신용 취소주문"),
120
+ # === 차트 (Charts) ===
121
+ "ka10060": ("/api/dostk/chart", "종목별투자자기관별차트요청"),
122
+ "ka10064": ("/api/dostk/chart", "장중투자자별매매차트요청"),
123
+ "ka10079": ("/api/dostk/chart", "주식틱차트조회요청"),
124
+ "ka10080": ("/api/dostk/chart", "주식분봉차트조회요청"),
125
+ "ka10081": ("/api/dostk/chart", "주식일봉차트조회요청"),
126
+ "ka10082": ("/api/dostk/chart", "주식주봉차트조회요청"),
127
+ "ka10083": ("/api/dostk/chart", "주식월봉차트조회요청"),
128
+ "ka10094": ("/api/dostk/chart", "주식년봉차트조회요청"),
129
+ "ka20004": ("/api/dostk/chart", "업종틱차트조회요청"),
130
+ "ka20005": ("/api/dostk/chart", "업종분봉조회요청"),
131
+ "ka20006": ("/api/dostk/chart", "업종일봉조회요청"),
132
+ "ka20007": ("/api/dostk/chart", "업종주봉조회요청"),
133
+ "ka20008": ("/api/dostk/chart", "업종월봉조회요청"),
134
+ "ka20019": ("/api/dostk/chart", "업종년봉조회요청"),
135
+ "ka50079": ("/api/dostk/chart", "금현물틱차트조회요청"),
136
+ "ka50080": ("/api/dostk/chart", "금현물분봉차트조회요청"),
137
+ "ka50081": ("/api/dostk/chart", "금현물일봉차트조회요청"),
138
+ "ka50082": ("/api/dostk/chart", "금현물주봉차트조회요청"),
139
+ "ka50083": ("/api/dostk/chart", "금현물월봉차트조회요청"),
140
+ "ka50091": ("/api/dostk/chart", "금현물당일틱차트조회요청"),
141
+ "ka50092": ("/api/dostk/chart", "금현물당일분봉차트조회요청"),
142
+ # === 순위정보 (Rankings) ===
143
+ "ka10020": ("/api/dostk/rkinfo", "호가잔량상위요청"),
144
+ "ka10021": ("/api/dostk/rkinfo", "호가잔량급증요청"),
145
+ "ka10022": ("/api/dostk/rkinfo", "잔량율급증요청"),
146
+ "ka10023": ("/api/dostk/rkinfo", "거래량급증요청"),
147
+ "ka10027": ("/api/dostk/rkinfo", "전일대비등락률상위요청"),
148
+ "ka10029": ("/api/dostk/rkinfo", "예상체결등락률상위요청"),
149
+ "ka10030": ("/api/dostk/rkinfo", "당일거래량상위요청"),
150
+ "ka10031": ("/api/dostk/rkinfo", "전일거래량상위요청"),
151
+ "ka10032": ("/api/dostk/rkinfo", "거래대금상위요청"),
152
+ "ka10033": ("/api/dostk/rkinfo", "신용비율상위요청"),
153
+ "ka10034": ("/api/dostk/rkinfo", "외인기간별매매상위요청"),
154
+ "ka10035": ("/api/dostk/rkinfo", "외인연속순매매상위요청"),
155
+ "ka10036": ("/api/dostk/rkinfo", "외인한도소진율증가상위"),
156
+ "ka10037": ("/api/dostk/rkinfo", "외국계창구매매상위요청"),
157
+ "ka10038": ("/api/dostk/rkinfo", "종목별증권사순위요청"),
158
+ "ka10039": ("/api/dostk/rkinfo", "증권사별매매상위요청"),
159
+ "ka10040": ("/api/dostk/rkinfo", "당일주요거래원요청"),
160
+ "ka10042": ("/api/dostk/rkinfo", "순매수거래원순위요청"),
161
+ "ka10053": ("/api/dostk/rkinfo", "당일상위이탈원요청"),
162
+ "ka10062": ("/api/dostk/rkinfo", "동일순매매순위요청"),
163
+ "ka10065": ("/api/dostk/rkinfo", "장중투자자별매매상위요청"),
164
+ "ka10098": ("/api/dostk/rkinfo", "시간외단일가등락율순위요청"),
165
+ "ka90009": ("/api/dostk/rkinfo", "외국인기관매매상위요청"),
166
+ # === 기관/외국인 (Foreign/Institutional) ===
167
+ "ka10008": ("/api/dostk/frgnistt", "주식외국인종목별매매동향"),
168
+ "ka10009": ("/api/dostk/frgnistt", "주식기관요청"),
169
+ "ka10131": ("/api/dostk/frgnistt", "기관외국인연속매매현황요청"),
170
+ "ka52301": ("/api/dostk/frgnistt", "금현물투자자현황"),
171
+ # === 업종 (Sector) ===
172
+ "ka10010": ("/api/dostk/sect", "업종프로그램요청"),
173
+ "ka10051": ("/api/dostk/sect", "업종별투자자순매수요청"),
174
+ "ka20001": ("/api/dostk/sect", "업종현재가요청"),
175
+ "ka20002": ("/api/dostk/sect", "업종별주가요청"),
176
+ "ka20003": ("/api/dostk/sect", "전업종지수요청"),
177
+ "ka20009": ("/api/dostk/sect", "업종현재가일별요청"),
178
+ # === ELW ===
179
+ "ka10048": ("/api/dostk/elw", "ELW일별민감도지표요청"),
180
+ "ka10050": ("/api/dostk/elw", "ELW민감도지표요청"),
181
+ "ka30001": ("/api/dostk/elw", "ELW가격급등락요청"),
182
+ "ka30002": ("/api/dostk/elw", "거래원별ELW순매매상위요청"),
183
+ "ka30003": ("/api/dostk/elw", "ELWLP보유일별추이요청"),
184
+ "ka30004": ("/api/dostk/elw", "ELW괴리율요청"),
185
+ "ka30005": ("/api/dostk/elw", "ELW조건검색요청"),
186
+ "ka30009": ("/api/dostk/elw", "ELW등락율순위요청"),
187
+ "ka30010": ("/api/dostk/elw", "ELW잔량순위요청"),
188
+ "ka30011": ("/api/dostk/elw", "ELW근접율요청"),
189
+ "ka30012": ("/api/dostk/elw", "ELW종목상세정보요청"),
190
+ # === ETF ===
191
+ "ka40001": ("/api/dostk/etf", "ETF수익율요청"),
192
+ "ka40002": ("/api/dostk/etf", "ETF종목정보요청"),
193
+ "ka40003": ("/api/dostk/etf", "ETF일별추이요청"),
194
+ "ka40004": ("/api/dostk/etf", "ETF전체시세요청"),
195
+ "ka40006": ("/api/dostk/etf", "ETF시간대별추이요청"),
196
+ "ka40007": ("/api/dostk/etf", "ETF시간대별체결요청"),
197
+ "ka40008": ("/api/dostk/etf", "ETF일자별체결요청"),
198
+ "ka40009": ("/api/dostk/etf", "ETF시간대별체결요청"),
199
+ "ka40010": ("/api/dostk/etf", "ETF시간대별추이요청"),
200
+ # === 공매도 (Short Selling) ===
201
+ "ka10014": ("/api/dostk/shsa", "공매도추이요청"),
202
+ # === 대차거래 (Securities Lending) ===
203
+ "ka10068": ("/api/dostk/slb", "대차거래추이요청"),
204
+ "ka10069": ("/api/dostk/slb", "대차거래상위10종목요청"),
205
+ "ka20068": ("/api/dostk/slb", "대차거래추이요청(종목별)"),
206
+ "ka90012": ("/api/dostk/slb", "대차거래내역요청"),
207
+ # === 테마 (Theme) ===
208
+ "ka90001": ("/api/dostk/thme", "테마그룹별요청"),
209
+ "ka90002": ("/api/dostk/thme", "테마구성종목요청"),
210
+ # === 조건검색 (Condition Search) / WebSocket ===
211
+ "ka10171": ("/api/dostk/websocket", "조건검색 목록조회"),
212
+ "ka10172": ("/api/dostk/websocket", "조건검색 요청 일반"),
213
+ "ka10173": ("/api/dostk/websocket", "조건검색 요청 실시간"),
214
+ "ka10174": ("/api/dostk/websocket", "조건검색 실시간 해제"),
215
+ }
216
+
217
+
218
+ def get_url(api_id: str) -> str:
219
+ entry = API_REGISTRY.get(api_id)
220
+ if entry is None:
221
+ raise ValueError(f"Unknown API ID: {api_id}")
222
+ return entry[0]
223
+
224
+
225
+ def get_description(api_id: str) -> str:
226
+ entry = API_REGISTRY.get(api_id)
227
+ if entry is None:
228
+ return api_id
229
+ return entry[1]
kiwoom_cli/auth.py ADDED
@@ -0,0 +1,30 @@
1
+ """OAuth token management for Kiwoom REST API.
2
+
3
+ Tokens are stored in ~/.kiwoom/token and loaded automatically by the client.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from pathlib import Path
9
+
10
+ from . import config
11
+
12
+ TOKEN_FILE = config.CONFIG_DIR / "token"
13
+
14
+
15
+ def save_token(token: str) -> None:
16
+ config.ensure_config_dir()
17
+ TOKEN_FILE.write_text(token)
18
+ TOKEN_FILE.chmod(0o600)
19
+
20
+
21
+ def load_token() -> str | None:
22
+ if TOKEN_FILE.exists():
23
+ t = TOKEN_FILE.read_text().strip()
24
+ return t if t else None
25
+ return None
26
+
27
+
28
+ def delete_token() -> None:
29
+ if TOKEN_FILE.exists():
30
+ TOKEN_FILE.unlink()
kiwoom_cli/client.py ADDED
@@ -0,0 +1,176 @@
1
+ """HTTP client for Kiwoom REST API.
2
+
3
+ Handles authentication headers, pagination, and error handling.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any
9
+
10
+ import click
11
+ import httpx
12
+
13
+ from . import auth, config
14
+ from .api_spec import get_url
15
+ from .output import err_console
16
+
17
+ CONTENT_TYPE = "application/json;charset=UTF-8"
18
+
19
+
20
+ class KiwoomAPIError(Exception):
21
+ def __init__(self, code: int, msg: str):
22
+ self.code = code
23
+ self.msg = msg
24
+ super().__init__(f"[{code}] {msg}")
25
+
26
+
27
+ class KiwoomClient:
28
+ """Synchronous client for the Kiwoom REST API."""
29
+
30
+ def __init__(self, domain: str | None = None, token: str | None = None):
31
+ self.domain = domain or config.get_domain()
32
+ self.token = token or auth.load_token()
33
+ self._http = httpx.Client(
34
+ base_url=self.domain,
35
+ timeout=30.0,
36
+ )
37
+
38
+ def close(self) -> None:
39
+ self._http.close()
40
+
41
+ def __enter__(self):
42
+ return self
43
+
44
+ def __exit__(self, *args):
45
+ self.close()
46
+
47
+ def _headers(self, api_id: str, cont_yn: str = "", next_key: str = "") -> dict:
48
+ h = {
49
+ "content-type": CONTENT_TYPE,
50
+ "api-id": api_id,
51
+ }
52
+ if self.token:
53
+ h["authorization"] = f"Bearer {self.token}"
54
+ if cont_yn:
55
+ h["cont-yn"] = cont_yn
56
+ if next_key:
57
+ h["next-key"] = next_key
58
+ return h
59
+
60
+ def _should_spin(self) -> bool:
61
+ """Show spinner only for table format on a real terminal."""
62
+ if not err_console.is_terminal:
63
+ return False
64
+ ctx = click.get_current_context(silent=True)
65
+ if ctx and ctx.obj and ctx.obj.get("format") != "table":
66
+ return False
67
+ return True
68
+
69
+ def request(
70
+ self,
71
+ api_id: str,
72
+ body: dict[str, Any] | None = None,
73
+ *,
74
+ cont_yn: str = "",
75
+ next_key: str = "",
76
+ ) -> tuple[dict[str, Any], dict[str, str]]:
77
+ """Make a single API request. Returns (body_json, response_headers)."""
78
+ url_path = get_url(api_id)
79
+ headers = self._headers(api_id, cont_yn, next_key)
80
+ if self._should_spin():
81
+ with err_console.status("[dim]조회 중...[/]", spinner="dots"):
82
+ resp = self._http.post(url_path, headers=headers, json=body or {})
83
+ else:
84
+ resp = self._http.post(url_path, headers=headers, json=body or {})
85
+ resp.raise_for_status()
86
+ data = resp.json()
87
+
88
+ rc = data.get("return_code")
89
+ if rc is not None and rc != 0:
90
+ raise KiwoomAPIError(rc, data.get("return_msg", "Unknown error"))
91
+
92
+ resp_headers = {
93
+ "cont-yn": resp.headers.get("cont-yn", ""),
94
+ "next-key": resp.headers.get("next-key", ""),
95
+ }
96
+ return data, resp_headers
97
+
98
+ def request_all(
99
+ self,
100
+ api_id: str,
101
+ body: dict[str, Any] | None = None,
102
+ *,
103
+ max_pages: int = 10,
104
+ ) -> list[dict[str, Any]]:
105
+ """Auto-paginate through cont-yn/next-key. Returns list of all response bodies."""
106
+ results = []
107
+ cont_yn = ""
108
+ next_key = ""
109
+
110
+ for _ in range(max_pages):
111
+ data, headers = self.request(
112
+ api_id, body, cont_yn=cont_yn, next_key=next_key
113
+ )
114
+ results.append(data)
115
+
116
+ if headers.get("cont-yn") == "Y" and headers.get("next-key"):
117
+ cont_yn = headers["cont-yn"]
118
+ next_key = headers["next-key"]
119
+ else:
120
+ break
121
+
122
+ return results
123
+
124
+ def issue_token(self, appkey: str | None = None, secretkey: str | None = None) -> str:
125
+ """Issue an access token via au10001."""
126
+ ak = appkey or config.get_appkey()
127
+ sk = secretkey or config.get_secretkey()
128
+ if not ak or not sk:
129
+ raise click.ClickException(
130
+ "appkey/secretkey not set. Run: kiwoom config setup"
131
+ )
132
+
133
+ resp = self._http.post(
134
+ "/oauth2/token",
135
+ headers={"content-type": CONTENT_TYPE, "api-id": "au10001"},
136
+ json={
137
+ "grant_type": "client_credentials",
138
+ "appkey": ak,
139
+ "secretkey": sk,
140
+ },
141
+ )
142
+ resp.raise_for_status()
143
+ data = resp.json()
144
+
145
+ rc = data.get("return_code")
146
+ if rc is not None and rc != 0:
147
+ raise KiwoomAPIError(rc, data.get("return_msg", "Token issue failed"))
148
+
149
+ token = data.get("token", "")
150
+ if not token:
151
+ # Some responses put it differently
152
+ for k in ("access_token", "token"):
153
+ if data.get(k):
154
+ token = data[k]
155
+ break
156
+
157
+ if token:
158
+ auth.save_token(token)
159
+ self.token = token
160
+ return token
161
+
162
+ def revoke_token(self) -> None:
163
+ """Revoke the current access token via au10002."""
164
+ ak = config.get_appkey()
165
+ sk = config.get_secretkey()
166
+ token = self.token or auth.load_token()
167
+ if not token:
168
+ raise click.ClickException("No token to revoke.")
169
+
170
+ self._http.post(
171
+ "/oauth2/revoke",
172
+ headers={"content-type": CONTENT_TYPE, "api-id": "au10002"},
173
+ json={"appkey": ak, "secretkey": sk, "token": token},
174
+ )
175
+ auth.delete_token()
176
+ self.token = None
File without changes