kwcli 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/__init__.py +47 -0
- kiwoom/_data/kiwoom_api_spec.json +66372 -0
- kiwoom/core/__init__.py +6 -0
- kiwoom/core/auth.py +349 -0
- kiwoom/core/client.py +244 -0
- kiwoom/core/errors.py +177 -0
- kiwoom/core/platform_paths.py +68 -0
- kiwoom/core/profiles.py +143 -0
- kiwoom/core/runtime.py +153 -0
- kiwoom/core/secrets.py +217 -0
- kiwoom/core/settings.py +64 -0
- kiwoom/core/token_store.py +186 -0
- kiwoom/core/types.py +28 -0
- kiwoom/core/ws_client.py +262 -0
- kiwoom/realtime/__init__.py +30 -0
- kiwoom/realtime/decoders.py +171 -0
- kiwoom/realtime/events.py +98 -0
- kiwoom/realtime/packets.py +65 -0
- kiwoom/realtime/schemas.py +76 -0
- kiwoom/realtime/stream.py +213 -0
- kiwoom/specs.py +272 -0
- kiwoom_cli/README.md +671 -0
- kiwoom_cli/__init__.py +9 -0
- kiwoom_cli/__main__.py +5 -0
- kiwoom_cli/argument_maps.py +561 -0
- kiwoom_cli/arguments.py +147 -0
- kiwoom_cli/auth_context.py +64 -0
- kiwoom_cli/banner.py +125 -0
- kiwoom_cli/commands/__init__.py +1 -0
- kiwoom_cli/commands/auth.py +406 -0
- kiwoom_cli/commands/groups.py +281 -0
- kiwoom_cli/commands/mapped.py +74 -0
- kiwoom_cli/commands/orders.py +158 -0
- kiwoom_cli/commands/spec.py +98 -0
- kiwoom_cli/commands/stocks.py +100 -0
- kiwoom_cli/commands/streams.py +470 -0
- kiwoom_cli/doctor.py +324 -0
- kiwoom_cli/errors.py +24 -0
- kiwoom_cli/executor/__init__.py +33 -0
- kiwoom_cli/executor/condition.py +374 -0
- kiwoom_cli/executor/rest.py +132 -0
- kiwoom_cli/executor/waits.py +34 -0
- kiwoom_cli/executor/websocket.py +131 -0
- kiwoom_cli/main.py +203 -0
- kiwoom_cli/maps/README.md +99 -0
- kiwoom_cli/maps/api_commands.csv +209 -0
- kiwoom_cli/maps/arguments.csv +731 -0
- kiwoom_cli/maps/order_confirmation_commands.csv +13 -0
- kiwoom_cli/maps/order_confirmation_fields.csv +71 -0
- kiwoom_cli/maps/order_price_policies.csv +47 -0
- kiwoom_cli/maps/order_value_labels.csv +28 -0
- kiwoom_cli/maps/positional_arguments.csv +21 -0
- kiwoom_cli/order_confirmation.py +167 -0
- kiwoom_cli/output.py +136 -0
- kiwoom_cli/registry.py +95 -0
- kiwoom_cli/safety.py +30 -0
- kiwoom_cli/setup.py +533 -0
- kwcli-0.1.0.dist-info/METADATA +215 -0
- kwcli-0.1.0.dist-info/RECORD +62 -0
- kwcli-0.1.0.dist-info/WHEEL +4 -0
- kwcli-0.1.0.dist-info/entry_points.txt +2 -0
- kwcli-0.1.0.dist-info/licenses/LICENSE.md +36 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
"""Account alias, auth status, token, and credential commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
from datetime import timedelta, timezone
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from kiwoom import SelectionContext, describe_selection, get_auth
|
|
12
|
+
from kiwoom.core.auth import (
|
|
13
|
+
KiwoomAuth,
|
|
14
|
+
endpoint_env_var_names,
|
|
15
|
+
get_base_url,
|
|
16
|
+
get_ws_base_url,
|
|
17
|
+
)
|
|
18
|
+
from kiwoom.core.platform_paths import protect_file
|
|
19
|
+
from kiwoom.core.profiles import (
|
|
20
|
+
delete_profile,
|
|
21
|
+
get_current_profile,
|
|
22
|
+
get_profile,
|
|
23
|
+
load_profiles,
|
|
24
|
+
set_current_profile,
|
|
25
|
+
)
|
|
26
|
+
from kiwoom.core.secrets import env_var_names
|
|
27
|
+
from kiwoom.core.types import VALID_MODES
|
|
28
|
+
from kiwoom_cli.arguments import resolve_optional_alias
|
|
29
|
+
from kiwoom_cli.auth_context import AuthContext, build_auth_context
|
|
30
|
+
from kiwoom_cli.errors import CliInputError
|
|
31
|
+
from kiwoom_cli.setup import run_login
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
KST = timezone(timedelta(hours=9))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def add_auth_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
38
|
+
auth_parser = subparsers.add_parser(
|
|
39
|
+
"auth",
|
|
40
|
+
help="계좌 별칭 등록/전환, 인증 상태 조회, 토큰 재발급, 서버 토큰 폐기, 로컬 인증 정보 정리를 수행합니다.",
|
|
41
|
+
)
|
|
42
|
+
auth_subparsers = auth_parser.add_subparsers(dest="auth_command", required=True)
|
|
43
|
+
|
|
44
|
+
login_parser = auth_subparsers.add_parser("login", help="계좌 별칭을 등록하거나 다시 설정합니다.")
|
|
45
|
+
login_parser.add_argument("alias_arg", nargs="?", metavar="alias", help="저장할 계좌 별칭")
|
|
46
|
+
login_parser.add_argument("--alias", help="저장할 계좌 별칭")
|
|
47
|
+
login_parser.add_argument("--mode", choices=VALID_MODES, help="계좌 별칭의 Kiwoom 모드")
|
|
48
|
+
login_parser.set_defaults(handler=handle_auth_login)
|
|
49
|
+
|
|
50
|
+
list_parser = auth_subparsers.add_parser(
|
|
51
|
+
"list", help="저장된 계좌 별칭 목록과 현재 선택된 계좌를 보여줍니다."
|
|
52
|
+
)
|
|
53
|
+
list_parser.set_defaults(handler=handle_auth_list)
|
|
54
|
+
|
|
55
|
+
switch_parser = auth_subparsers.add_parser("switch", help="현재 계좌 별칭을 전환합니다.")
|
|
56
|
+
switch_parser.add_argument("alias", help="전환할 계좌 별칭")
|
|
57
|
+
switch_parser.set_defaults(handler=handle_auth_switch)
|
|
58
|
+
|
|
59
|
+
status_parser = auth_subparsers.add_parser("status", help="자격 증명과 토큰 상태를 확인합니다.")
|
|
60
|
+
_add_auth_target_args(status_parser)
|
|
61
|
+
status_parser.set_defaults(handler=handle_auth_status)
|
|
62
|
+
|
|
63
|
+
refresh_parser = auth_subparsers.add_parser(
|
|
64
|
+
"refresh",
|
|
65
|
+
help="저장된 자격 증명으로 새 접근 토큰을 발급하고 로컬 캐시를 갱신합니다.",
|
|
66
|
+
)
|
|
67
|
+
_add_auth_target_args(refresh_parser)
|
|
68
|
+
refresh_parser.set_defaults(handler=handle_auth_refresh)
|
|
69
|
+
|
|
70
|
+
revoke_parser = auth_subparsers.add_parser(
|
|
71
|
+
"revoke",
|
|
72
|
+
help="로컬 토큰 캐시에 저장된 현재 토큰을 서버에서 폐기하고 로컬 캐시도 삭제합니다.",
|
|
73
|
+
)
|
|
74
|
+
_add_auth_target_args(revoke_parser)
|
|
75
|
+
revoke_parser.set_defaults(handler=handle_auth_revoke)
|
|
76
|
+
|
|
77
|
+
clear_parser = auth_subparsers.add_parser("clear", help="로컬 토큰 캐시를 삭제합니다.")
|
|
78
|
+
_add_auth_target_args(clear_parser)
|
|
79
|
+
clear_parser.add_argument(
|
|
80
|
+
"--all",
|
|
81
|
+
dest="clear_all",
|
|
82
|
+
action="store_true",
|
|
83
|
+
help="로컬 토큰 캐시와 운영체제 자격 증명 저장소의 값을 삭제합니다. 환경변수는 삭제하지 않습니다.",
|
|
84
|
+
)
|
|
85
|
+
clear_parser.add_argument(
|
|
86
|
+
"--credentials",
|
|
87
|
+
dest="clear_all",
|
|
88
|
+
action="store_true",
|
|
89
|
+
help=argparse.SUPPRESS,
|
|
90
|
+
)
|
|
91
|
+
clear_parser.set_defaults(handler=handle_auth_clear)
|
|
92
|
+
|
|
93
|
+
remove_parser = auth_subparsers.add_parser(
|
|
94
|
+
"remove",
|
|
95
|
+
help="계좌 별칭 등록을 삭제합니다(토큰 캐시와 자격 증명도 함께 삭제).",
|
|
96
|
+
)
|
|
97
|
+
remove_parser.add_argument("alias", help="삭제할 계좌 별칭")
|
|
98
|
+
remove_parser.set_defaults(handler=handle_auth_remove)
|
|
99
|
+
|
|
100
|
+
export_parser = auth_subparsers.add_parser(
|
|
101
|
+
"export",
|
|
102
|
+
help="저장된 App Key/Secret을 파일(.env)로 내보냅니다(화면에는 출력하지 않음).",
|
|
103
|
+
)
|
|
104
|
+
_add_auth_target_args(export_parser)
|
|
105
|
+
export_parser.add_argument(
|
|
106
|
+
"--dir", help="내보낼 폴더. 생략하면 현재 폴더에 저장하며 확인을 요청합니다."
|
|
107
|
+
)
|
|
108
|
+
export_parser.add_argument(
|
|
109
|
+
"--yes", action="store_true", help="내보내기 확인 프롬프트를 생략합니다."
|
|
110
|
+
)
|
|
111
|
+
export_parser.set_defaults(handler=handle_auth_export)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def handle_auth_login(args: argparse.Namespace) -> None:
|
|
115
|
+
run_login(
|
|
116
|
+
alias=resolve_optional_alias(args.alias_arg, args.alias, "--alias"),
|
|
117
|
+
mode=args.mode,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def handle_auth_list(args: argparse.Namespace) -> None:
|
|
122
|
+
_print_profile_list()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def handle_auth_switch(args: argparse.Namespace) -> None:
|
|
126
|
+
profile = set_current_profile(args.alias)
|
|
127
|
+
print(f"현재 계좌 별칭: {profile.alias} ({profile.mode})")
|
|
128
|
+
print(f"다음 단계: kiwoomcli auth status --profile {profile.alias}")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def handle_auth_remove(args: argparse.Namespace) -> None:
|
|
132
|
+
profile = get_profile(args.alias)
|
|
133
|
+
was_current = bool(
|
|
134
|
+
get_current_profile() and get_current_profile().alias == profile.alias
|
|
135
|
+
)
|
|
136
|
+
auth = get_auth(profile=profile.alias)
|
|
137
|
+
auth.clear_token()
|
|
138
|
+
auth.secret_provider.clear_credentials(profile.mode)
|
|
139
|
+
delete_profile(profile.alias)
|
|
140
|
+
print(f"계좌 별칭 {profile.alias} ({profile.mode})을(를) 삭제했습니다.")
|
|
141
|
+
print("등록, 토큰 캐시, 운영체제 자격 증명 저장소의 값을 모두 제거했습니다.")
|
|
142
|
+
if was_current:
|
|
143
|
+
print("현재 선택된 계좌였으므로 현재 계좌 선택도 해제했습니다.")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def handle_auth_export(args: argparse.Namespace) -> None:
|
|
147
|
+
auth, auth_label = _resolve_auth(args)
|
|
148
|
+
credentials = auth.secret_provider.get_credentials(auth.mode)
|
|
149
|
+
if credentials is None:
|
|
150
|
+
raise CliInputError(
|
|
151
|
+
f"{auth_label}: 내보낼 App Key/Secret이 없습니다. 먼저 kiwoomcli setup으로 저장하세요."
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
target = auth.profile or auth.mode
|
|
155
|
+
used_default_dir = not args.dir
|
|
156
|
+
dest_dir = Path(args.dir).expanduser() if args.dir else Path.cwd()
|
|
157
|
+
dest_file = dest_dir / f"kiwoom_{target}.env"
|
|
158
|
+
|
|
159
|
+
if used_default_dir:
|
|
160
|
+
print(f"저장 위치를 지정하지 않아 현재 폴더에 저장합니다: {dest_file}")
|
|
161
|
+
else:
|
|
162
|
+
print(f"저장 위치: {dest_file}")
|
|
163
|
+
|
|
164
|
+
if not _confirm_export(args):
|
|
165
|
+
print("내보내기를 취소했습니다.")
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
169
|
+
_write_credentials_file(dest_file, auth.mode, credentials, auth_label)
|
|
170
|
+
gitignore_note = _register_gitignore(dest_file)
|
|
171
|
+
|
|
172
|
+
permission_note = (
|
|
173
|
+
"현재 사용자 전용 (Windows ACL)"
|
|
174
|
+
if os.name == "nt"
|
|
175
|
+
else "0600 (소유자 전용)"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
print()
|
|
179
|
+
print(f"{auth_label} App Key/Secret을 파일로 내보냈습니다.")
|
|
180
|
+
print(f" 파일: {dest_file}")
|
|
181
|
+
print(f" 권한: {permission_note}")
|
|
182
|
+
print(f" .gitignore: {gitignore_note}")
|
|
183
|
+
print("주의: 평문 자격증명 파일입니다. 공유하거나 커밋하지 마세요.")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _confirm_export(args: argparse.Namespace) -> bool:
|
|
187
|
+
if getattr(args, "yes", False):
|
|
188
|
+
return True
|
|
189
|
+
if not sys.stdin.isatty():
|
|
190
|
+
raise CliInputError(
|
|
191
|
+
"비대화형 환경에서 자격증명 내보내기는 --yes 옵션이 필요합니다."
|
|
192
|
+
)
|
|
193
|
+
print("경고: App Key/Secret을 평문 파일로 저장합니다.")
|
|
194
|
+
return input("정말 내보내시겠습니까? [y/N] ").strip().lower() in {"y", "yes"}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _write_credentials_file(
|
|
198
|
+
path: Path, mode: str, credentials, auth_label: str
|
|
199
|
+
) -> None:
|
|
200
|
+
appkey_var, secretkey_var = env_var_names(mode)
|
|
201
|
+
content = "\n".join(
|
|
202
|
+
[
|
|
203
|
+
f"# kiwoomcli App Key/Secret export - {auth_label}",
|
|
204
|
+
"# 평문 자격증명입니다. 절대 공유/커밋하지 마세요.",
|
|
205
|
+
f"{appkey_var}={credentials.appkey}",
|
|
206
|
+
f"{secretkey_var}={credentials.secretkey}",
|
|
207
|
+
"",
|
|
208
|
+
]
|
|
209
|
+
)
|
|
210
|
+
# POSIX에서는 생성 순간부터 0600으로 열어 넓은 권한 노출 창을 없앤다.
|
|
211
|
+
# (Windows에서는 mode 인자가 사실상 무시되므로 아래 protect_file이 실제 제한을 담당한다.)
|
|
212
|
+
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
213
|
+
with os.fdopen(fd, "w", encoding="utf-8") as file:
|
|
214
|
+
file.write(content)
|
|
215
|
+
# 토큰/프로필 저장과 동일한 공용 보호를 적용한다.
|
|
216
|
+
# POSIX: chmod 0600, Windows: icacls로 상속 제거 후 현재 사용자 전용.
|
|
217
|
+
protect_file(path)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _register_gitignore(file_path: Path) -> str:
|
|
221
|
+
repo_root = _find_repo_root(file_path.parent.resolve())
|
|
222
|
+
if repo_root is None:
|
|
223
|
+
return "git 저장소가 아니라 등록을 건너뜁니다"
|
|
224
|
+
gitignore = repo_root / ".gitignore"
|
|
225
|
+
try:
|
|
226
|
+
relative = file_path.resolve().relative_to(repo_root)
|
|
227
|
+
except ValueError:
|
|
228
|
+
entry = file_path.name
|
|
229
|
+
else:
|
|
230
|
+
entry = "/" + str(relative).replace(os.sep, "/")
|
|
231
|
+
existing = (
|
|
232
|
+
gitignore.read_text(encoding="utf-8").splitlines()
|
|
233
|
+
if gitignore.exists()
|
|
234
|
+
else []
|
|
235
|
+
)
|
|
236
|
+
if entry in existing or file_path.name in existing:
|
|
237
|
+
return f"이미 등록됨 ({gitignore})"
|
|
238
|
+
with gitignore.open("a", encoding="utf-8") as file:
|
|
239
|
+
if existing and existing[-1].strip() != "":
|
|
240
|
+
file.write("\n")
|
|
241
|
+
file.write("# kiwoomcli exported credentials (do not commit)\n")
|
|
242
|
+
file.write(f"{entry}\n")
|
|
243
|
+
return f"{entry} 등록됨 ({gitignore})"
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _find_repo_root(start: Path) -> Path | None:
|
|
247
|
+
for parent in (start, *start.parents):
|
|
248
|
+
if (parent / ".git").exists():
|
|
249
|
+
return parent
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def handle_auth_status(args: argparse.Namespace) -> None:
|
|
254
|
+
auth, auth_label = _resolve_auth(args)
|
|
255
|
+
selection = describe_selection(
|
|
256
|
+
getattr(args, "mode", None), profile=getattr(args, "profile", None)
|
|
257
|
+
)
|
|
258
|
+
_print_auth_status(auth, auth_label, selection)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def handle_auth_refresh(args: argparse.Namespace) -> None:
|
|
262
|
+
auth, auth_label = _resolve_auth(args)
|
|
263
|
+
auth.refresh_access_token()
|
|
264
|
+
status = auth.status()
|
|
265
|
+
print(f"{auth_label} 새 접근 토큰을 발급했습니다.")
|
|
266
|
+
print(f"토큰 저장 시각: {_format_kst_datetime(status.token_saved_at, none_label='알 수 없음')}")
|
|
267
|
+
print(f"토큰 만료 시각: {_format_kst_datetime(status.token_expires_at, none_label='알 수 없음')}")
|
|
268
|
+
print(f"다음 단계: kiwoomcli auth status{_auth_target_hint(auth)}")
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def handle_auth_revoke(args: argparse.Namespace) -> None:
|
|
272
|
+
auth, auth_label = _resolve_auth(args)
|
|
273
|
+
auth.revoke_access_token()
|
|
274
|
+
print(f"{auth_label} 로컬 토큰 캐시에 저장된 현재 토큰을 서버에서 폐기했습니다.")
|
|
275
|
+
print("로컬 토큰 캐시도 삭제했습니다.")
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def handle_auth_clear(args: argparse.Namespace) -> None:
|
|
279
|
+
auth, auth_label = _resolve_auth(args)
|
|
280
|
+
auth.clear_token()
|
|
281
|
+
if args.clear_all:
|
|
282
|
+
auth.secret_provider.clear_credentials(auth.mode)
|
|
283
|
+
print(f"{auth_label} 토큰 캐시를 삭제했습니다.")
|
|
284
|
+
if args.clear_all:
|
|
285
|
+
print("운영체제 자격 증명 저장소의 값도 삭제했습니다.")
|
|
286
|
+
if auth.profile is None:
|
|
287
|
+
appkey_var, secretkey_var = env_var_names(auth.mode)
|
|
288
|
+
remaining_env = [name for name in (appkey_var, secretkey_var) if os.getenv(name)]
|
|
289
|
+
if remaining_env:
|
|
290
|
+
print("다음 환경변수는 여전히 우선 적용됩니다.")
|
|
291
|
+
for name in remaining_env:
|
|
292
|
+
print(f"- {name}")
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _resolve_auth(args: argparse.Namespace) -> tuple[KiwoomAuth, str]:
|
|
296
|
+
if hasattr(args, "profile_arg"):
|
|
297
|
+
args.profile = resolve_optional_alias(args.profile_arg, args.profile, "--profile")
|
|
298
|
+
auth: KiwoomAuth = get_auth(
|
|
299
|
+
mode=getattr(args, "mode", None), profile=getattr(args, "profile", None)
|
|
300
|
+
)
|
|
301
|
+
return auth, _auth_label(auth)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _add_auth_target_args(parser: argparse.ArgumentParser) -> None:
|
|
305
|
+
parser.add_argument(
|
|
306
|
+
"profile_arg",
|
|
307
|
+
nargs="?",
|
|
308
|
+
metavar="alias",
|
|
309
|
+
help="대상 계좌 별칭입니다. --profile 대신 사용할 수 있습니다.",
|
|
310
|
+
)
|
|
311
|
+
parser.add_argument(
|
|
312
|
+
"--profile",
|
|
313
|
+
help="사용할 계좌 별칭입니다. --mode/KIWOOM_MODE가 없을 때 생략하면 현재 계좌 별칭을 사용합니다.",
|
|
314
|
+
)
|
|
315
|
+
parser.add_argument("--mode", choices=VALID_MODES, help="계좌 별칭 대신 모드를 직접 지정합니다.")
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _print_profile_list() -> None:
|
|
319
|
+
profiles = load_profiles()
|
|
320
|
+
current = get_current_profile()
|
|
321
|
+
if not profiles:
|
|
322
|
+
print("저장된 계좌 별칭이 없습니다.")
|
|
323
|
+
print("다음 단계: kiwoomcli auth login")
|
|
324
|
+
return
|
|
325
|
+
|
|
326
|
+
print("계좌 별칭 목록:")
|
|
327
|
+
for alias in sorted(profiles):
|
|
328
|
+
profile = profiles[alias]
|
|
329
|
+
marker = "*" if current is not None and current.alias == alias else " "
|
|
330
|
+
# Pass the profile's own mode so ambient KIWOOM_MODE can't trigger a
|
|
331
|
+
# mode-mismatch while listing stored profiles.
|
|
332
|
+
context = build_auth_context(mode=profile.mode, profile=alias)
|
|
333
|
+
status = context.status
|
|
334
|
+
credential_label = "credentials=예" if status.has_credentials else "credentials=아니오"
|
|
335
|
+
token_label = "token=재사용 가능" if status.token_reusable else "token=재발급 필요" if status.has_token else "token=없음"
|
|
336
|
+
call_label = "지금 호출=가능" if context.can_call_now else "지금 호출=불가"
|
|
337
|
+
refresh_label = "재발급=가능" if context.can_refresh else "재발급=불가"
|
|
338
|
+
print(
|
|
339
|
+
f"{marker} {profile.alias}\tmode={profile.mode}\t{credential_label}\t"
|
|
340
|
+
f"{token_label}\t{call_label}\t{refresh_label}"
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _print_auth_status(auth: KiwoomAuth, auth_label: str, selection: SelectionContext) -> None:
|
|
345
|
+
context = AuthContext(selection=selection, status=auth.status())
|
|
346
|
+
status = context.status
|
|
347
|
+
credential_source = {
|
|
348
|
+
"env": "환경변수",
|
|
349
|
+
"keyring": "운영체제 자격 증명 저장소",
|
|
350
|
+
None: "없음",
|
|
351
|
+
}.get(status.credential_source, status.credential_source or "없음")
|
|
352
|
+
print(f"선택 방식: {selection.selection_source}")
|
|
353
|
+
if status.profile:
|
|
354
|
+
print(f"계좌 별칭: {status.profile}")
|
|
355
|
+
print(f"모드: {status.mode}")
|
|
356
|
+
print(f"대상: {auth_label}")
|
|
357
|
+
_print_endpoint_status(status.mode)
|
|
358
|
+
print(f"자격 증명 존재: {'예' if status.has_credentials else '아니오'}")
|
|
359
|
+
print(f"자격 증명 출처: {credential_source}")
|
|
360
|
+
print(f"토큰 존재: {'예' if status.has_token else '아니오'}")
|
|
361
|
+
print(f"토큰 유효: {'예' if status.token_valid else '아니오'}")
|
|
362
|
+
print(f"토큰 재사용 가능: {'예' if status.token_reusable else '아니오'}")
|
|
363
|
+
print(f"토큰 저장 시각: {_format_kst_datetime(status.token_saved_at)}")
|
|
364
|
+
print(f"토큰 만료 시각: {_format_kst_datetime(status.token_expires_at)}")
|
|
365
|
+
print(f"다음 API 호출 인증 동작: {_next_auth_action_label(status.next_auth_action)}")
|
|
366
|
+
print(f"지금 API 호출 가능: {'예' if context.can_call_now else '아니오'}")
|
|
367
|
+
print(f"토큰 재발급 가능: {'예' if context.can_refresh else '아니오'}")
|
|
368
|
+
for warning in context.warnings():
|
|
369
|
+
print(f"주의: {warning}")
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _print_endpoint_status(mode: str) -> None:
|
|
373
|
+
rest_var, ws_var = endpoint_env_var_names(mode)
|
|
374
|
+
print(f"REST 주소: {get_base_url(mode)} ({rest_var}, {_endpoint_source_label(rest_var)})")
|
|
375
|
+
print(f"WebSocket 주소: {get_ws_base_url(mode)} ({ws_var}, {_endpoint_source_label(ws_var)})")
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _endpoint_source_label(env_name: str) -> str:
|
|
379
|
+
return "환경변수" if os.getenv(env_name) else "기본값"
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _auth_label(auth: KiwoomAuth) -> str:
|
|
383
|
+
if auth.profile:
|
|
384
|
+
return f"계좌 별칭 {auth.profile} ({auth.mode})"
|
|
385
|
+
return f"{auth.mode} mode"
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _auth_target_hint(auth: KiwoomAuth) -> str:
|
|
389
|
+
if auth.profile:
|
|
390
|
+
return f" --profile {auth.profile}"
|
|
391
|
+
return f" --mode {auth.mode}"
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _format_kst_datetime(value, *, none_label: str = "없음") -> str:
|
|
395
|
+
if value is None:
|
|
396
|
+
return none_label
|
|
397
|
+
return f"{value.astimezone(KST).isoformat()} (KST)"
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _next_auth_action_label(action: str) -> str:
|
|
401
|
+
return {
|
|
402
|
+
"use_cached_token": "캐시 토큰 사용",
|
|
403
|
+
"issue_before_request": "다음 API 호출 시 새 토큰 자동 발급",
|
|
404
|
+
"refresh_before_request": "다음 API 호출 시 토큰 자동 재발급",
|
|
405
|
+
"credentials_missing": "자격 증명 필요",
|
|
406
|
+
}.get(action, action)
|