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.
Files changed (62) hide show
  1. kiwoom/__init__.py +47 -0
  2. kiwoom/_data/kiwoom_api_spec.json +66372 -0
  3. kiwoom/core/__init__.py +6 -0
  4. kiwoom/core/auth.py +349 -0
  5. kiwoom/core/client.py +244 -0
  6. kiwoom/core/errors.py +177 -0
  7. kiwoom/core/platform_paths.py +68 -0
  8. kiwoom/core/profiles.py +143 -0
  9. kiwoom/core/runtime.py +153 -0
  10. kiwoom/core/secrets.py +217 -0
  11. kiwoom/core/settings.py +64 -0
  12. kiwoom/core/token_store.py +186 -0
  13. kiwoom/core/types.py +28 -0
  14. kiwoom/core/ws_client.py +262 -0
  15. kiwoom/realtime/__init__.py +30 -0
  16. kiwoom/realtime/decoders.py +171 -0
  17. kiwoom/realtime/events.py +98 -0
  18. kiwoom/realtime/packets.py +65 -0
  19. kiwoom/realtime/schemas.py +76 -0
  20. kiwoom/realtime/stream.py +213 -0
  21. kiwoom/specs.py +272 -0
  22. kiwoom_cli/README.md +671 -0
  23. kiwoom_cli/__init__.py +9 -0
  24. kiwoom_cli/__main__.py +5 -0
  25. kiwoom_cli/argument_maps.py +561 -0
  26. kiwoom_cli/arguments.py +147 -0
  27. kiwoom_cli/auth_context.py +64 -0
  28. kiwoom_cli/banner.py +125 -0
  29. kiwoom_cli/commands/__init__.py +1 -0
  30. kiwoom_cli/commands/auth.py +406 -0
  31. kiwoom_cli/commands/groups.py +281 -0
  32. kiwoom_cli/commands/mapped.py +74 -0
  33. kiwoom_cli/commands/orders.py +158 -0
  34. kiwoom_cli/commands/spec.py +98 -0
  35. kiwoom_cli/commands/stocks.py +100 -0
  36. kiwoom_cli/commands/streams.py +470 -0
  37. kiwoom_cli/doctor.py +324 -0
  38. kiwoom_cli/errors.py +24 -0
  39. kiwoom_cli/executor/__init__.py +33 -0
  40. kiwoom_cli/executor/condition.py +374 -0
  41. kiwoom_cli/executor/rest.py +132 -0
  42. kiwoom_cli/executor/waits.py +34 -0
  43. kiwoom_cli/executor/websocket.py +131 -0
  44. kiwoom_cli/main.py +203 -0
  45. kiwoom_cli/maps/README.md +99 -0
  46. kiwoom_cli/maps/api_commands.csv +209 -0
  47. kiwoom_cli/maps/arguments.csv +731 -0
  48. kiwoom_cli/maps/order_confirmation_commands.csv +13 -0
  49. kiwoom_cli/maps/order_confirmation_fields.csv +71 -0
  50. kiwoom_cli/maps/order_price_policies.csv +47 -0
  51. kiwoom_cli/maps/order_value_labels.csv +28 -0
  52. kiwoom_cli/maps/positional_arguments.csv +21 -0
  53. kiwoom_cli/order_confirmation.py +167 -0
  54. kiwoom_cli/output.py +136 -0
  55. kiwoom_cli/registry.py +95 -0
  56. kiwoom_cli/safety.py +30 -0
  57. kiwoom_cli/setup.py +533 -0
  58. kwcli-0.1.0.dist-info/METADATA +215 -0
  59. kwcli-0.1.0.dist-info/RECORD +62 -0
  60. kwcli-0.1.0.dist-info/WHEEL +4 -0
  61. kwcli-0.1.0.dist-info/entry_points.txt +2 -0
  62. kwcli-0.1.0.dist-info/licenses/LICENSE.md +36 -0
kiwoom_cli/doctor.py ADDED
@@ -0,0 +1,324 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import os
5
+ import platform
6
+ import shlex
7
+ import shutil
8
+ import stat
9
+ import sys
10
+ from dataclasses import dataclass, field
11
+ from pathlib import Path
12
+
13
+ from kiwoom.core.errors import KiwoomError, ModeNotConfiguredError
14
+ from kiwoom.core.platform_paths import cache_dir
15
+ from kiwoom.core.profiles import get_current_profile, load_profiles
16
+ from kiwoom.core.runtime import describe_selection
17
+ from kiwoom.core.secrets import env_var_names
18
+ from kiwoom.core.settings import get_mode_from_env, get_profile_from_env, settings_path
19
+ from kiwoom_cli.auth_context import build_auth_context
20
+
21
+
22
+ def add_doctor_parser(subparsers: argparse._SubParsersAction) -> None:
23
+ doctor_parser = subparsers.add_parser(
24
+ "doctor", help="설치/인증 컨텍스트 현재 상태를 진단합니다."
25
+ )
26
+ doctor_parser.set_defaults(handler=handle_doctor)
27
+
28
+
29
+ def handle_doctor(args: argparse.Namespace) -> None:
30
+ print_doctor()
31
+
32
+
33
+ # 설치된 kiwoom은 자체 격리 런타임에서 돌기 때문에 이 도구들은 실행 필수가 아니라
34
+ # 설치/업데이트나 부가 기능(예: auth export의 .gitignore 등록)용 온보딩 항목이다.
35
+ # 따라서 Windows에서만 존재 여부를 경고성으로 안내한다.
36
+ WINDOWS_ONBOARDING_TOOLS = ("git", "python", "uv")
37
+
38
+
39
+ def os_label() -> str:
40
+ system = platform.system()
41
+ if system == "Darwin":
42
+ return f"macOS {platform.mac_ver()[0] or platform.release()}".strip()
43
+ if system == "Windows":
44
+ return f"Windows {platform.release()}".strip()
45
+ if system == "Linux":
46
+ return f"Linux {platform.release()}".strip()
47
+ return system or "알 수 없음"
48
+
49
+
50
+ def _tool_found(tool: str) -> bool:
51
+ # Windows에서 python은 python/python3 외에 py 런처로도 제공된다.
52
+ candidates = {"python": ("python", "python3", "py")}.get(tool, (tool,))
53
+ return any(shutil.which(candidate) for candidate in candidates)
54
+
55
+
56
+ def onboarding_tool_report() -> list[tuple[str, bool]]:
57
+ """Windows에서만 (도구, 발견여부) 목록을 돌려준다. 다른 OS에서는 빈 목록."""
58
+ if os.name != "nt":
59
+ return []
60
+ return [(tool, _tool_found(tool)) for tool in WINDOWS_ONBOARDING_TOOLS]
61
+
62
+
63
+ def command_path() -> str:
64
+ return shutil.which("kiwoom") or "없음"
65
+
66
+
67
+ def current_executable() -> str:
68
+ raw = sys.argv[0] or ""
69
+ if not raw:
70
+ return "알 수 없음"
71
+ path = Path(raw)
72
+ if not path.is_absolute():
73
+ path = Path.cwd() / path
74
+ return str(path)
75
+
76
+
77
+ def resolved_current_executable() -> str:
78
+ raw = current_executable()
79
+ if raw in {"", "알 수 없음"}:
80
+ return raw
81
+ return str(Path(raw).resolve())
82
+
83
+
84
+ def path_kiwoom_entries() -> list[str]:
85
+ entries: list[str] = []
86
+ seen: set[str] = set()
87
+ for directory in os.environ.get("PATH", "").split(os.pathsep):
88
+ if not directory:
89
+ continue
90
+ candidate = Path(directory) / "kiwoom"
91
+ try:
92
+ candidate_stat = candidate.stat()
93
+ except OSError:
94
+ continue
95
+ if not stat.S_ISREG(candidate_stat.st_mode):
96
+ continue
97
+ if not os.access(candidate, os.X_OK):
98
+ continue
99
+ rendered = str(candidate)
100
+ if rendered not in seen:
101
+ entries.append(rendered)
102
+ seen.add(rendered)
103
+ return entries
104
+
105
+
106
+ @dataclass
107
+ class _Finding:
108
+ """A diagnosed problem: symptom plus the exact command(s) to fix it."""
109
+
110
+ symptom: str
111
+ fixes: list[str] = field(default_factory=list)
112
+
113
+
114
+ def print_doctor() -> None:
115
+ """Diagnose the current auth context. Verdict-first and never crashes.
116
+
117
+ Every gathering step is defensive: a broken settings file or a conflicting
118
+ environment becomes a reported finding, not a traceback — because doctor is
119
+ exactly the command a user runs when those things are wrong.
120
+ """
121
+ print("Kiwoom CLI 진단")
122
+ print()
123
+
124
+ findings: list[_Finding] = []
125
+ target_label, can_call_now, cause = _diagnose_target(findings)
126
+ _diagnose_install(findings)
127
+
128
+ _print_verdict(target_label, can_call_now, cause)
129
+ print()
130
+ _print_findings(findings)
131
+ print()
132
+ _print_detail()
133
+ print()
134
+ _print_environment_summary()
135
+
136
+
137
+ def _print_verdict(target_label: str, can_call_now: bool | None, cause: str | None) -> None:
138
+ print("판정")
139
+ print(f" 유효 대상: {target_label}")
140
+ if can_call_now is None:
141
+ print(" 지금 호출 가능: 알 수 없음")
142
+ else:
143
+ print(f" 지금 호출 가능: {'예' if can_call_now else '아니오'}")
144
+ if cause:
145
+ print(f" 원인: {cause}")
146
+
147
+
148
+ def _print_findings(findings: list[_Finding]) -> None:
149
+ print("발견된 문제")
150
+ if not findings:
151
+ print(" 특이 사항이 없습니다.")
152
+ return
153
+ for finding in findings:
154
+ print(f" • {finding.symptom}")
155
+ for fix in finding.fixes:
156
+ print(f" 고치려면: {fix}")
157
+
158
+
159
+ def _diagnose_target(findings: list[_Finding]) -> tuple[str, bool | None, str | None]:
160
+ """Resolve the default target and judge whether it can call now.
161
+
162
+ Returns (target_label, can_call_now, cause). can_call_now is None when the
163
+ target cannot be resolved (not configured / conflicting / unreadable).
164
+ """
165
+ current = _safe_current_profile(findings)
166
+
167
+ try:
168
+ selection = describe_selection()
169
+ except ModeNotConfiguredError:
170
+ findings.append(_Finding("기본 실행 대상이 설정되지 않았습니다.", ["kiwoomcli setup"]))
171
+ return ("없음", None, "설정된 계좌 별칭 또는 모드가 없습니다")
172
+ except ValueError as exc:
173
+ # KIWOOM_PROFILE + KIWOOM_MODE point at conflicting modes.
174
+ findings.append(
175
+ _Finding(
176
+ f"환경변수 충돌: {exc}",
177
+ ["KIWOOM_MODE 또는 KIWOOM_PROFILE 중 하나를 unset 하세요"],
178
+ )
179
+ )
180
+ return ("알 수 없음", None, "KIWOOM_PROFILE 과 KIWOOM_MODE 가 충돌합니다")
181
+ except KiwoomError as exc:
182
+ # e.g. KIWOOM_PROFILE names a missing profile, or settings unreadable.
183
+ findings.append(_Finding(f"실행 대상을 해석할 수 없습니다: {exc}", ["kiwoomcli setup", "또는 KIWOOM_PROFILE 값 확인"]))
184
+ return ("알 수 없음", None, str(exc))
185
+
186
+ _append_override_finding(findings, current, selection)
187
+
188
+ try:
189
+ context = build_auth_context()
190
+ except KiwoomError as exc:
191
+ return (selection.target_label, None, f"상태 확인 실패: {exc}")
192
+
193
+ if context.can_call_now:
194
+ return (selection.target_label, True, None)
195
+
196
+ status = context.status
197
+ cause = "키/시크릿이 없고 사용 가능한 토큰도 없습니다"
198
+ if selection.uses_profile:
199
+ alias_arg = shlex.quote(str(selection.profile))
200
+ fixes = [
201
+ f"kiwoomcli auth login --alias {alias_arg} --mode {selection.mode}",
202
+ f"kiwoomcli auth status --profile {alias_arg}",
203
+ ]
204
+ if status.has_token and not status.token_reusable and not status.has_credentials:
205
+ symptom = (
206
+ f"계좌 별칭 '{selection.profile}' 은 남아 있지만 저장된 키/시크릿을 "
207
+ "읽을 수 없고 토큰도 재사용할 수 없습니다."
208
+ )
209
+ else:
210
+ symptom = f"유효 대상 '{selection.profile}' 으로 지금 호출할 수 없습니다."
211
+ findings.append(
212
+ _Finding(
213
+ symptom,
214
+ fixes,
215
+ )
216
+ )
217
+ else:
218
+ appkey_var, secretkey_var = env_var_names(selection.mode)
219
+ findings.append(
220
+ _Finding(
221
+ f"유효 대상 {selection.mode} mode 로 지금 호출할 수 없습니다 (키/시크릿 없음).",
222
+ ["kiwoomcli setup", f"{appkey_var} / {secretkey_var} 환경변수 설정"],
223
+ )
224
+ )
225
+ return (selection.target_label, False, cause)
226
+
227
+
228
+ def _append_override_finding(findings: list[_Finding], current, selection) -> None:
229
+ """Flag when an environment variable overrides the configured current profile.
230
+
231
+ This is the "demo인데 왜 real?" surprise: a stored current_profile is
232
+ silently bypassed because KIWOOM_MODE / KIWOOM_PROFILE wins the precedence.
233
+ """
234
+ if current is None:
235
+ return
236
+ if selection.selection_source not in {"KIWOOM_MODE", "KIWOOM_PROFILE"}:
237
+ return
238
+ if selection.uses_profile and selection.profile == current.alias:
239
+ return
240
+ findings.append(
241
+ _Finding(
242
+ f"{selection.selection_source} 가 current_profile {current.alias}({current.mode})"
243
+ f" 보다 우선 적용되어 유효 대상이 {selection.target_label} 입니다.",
244
+ [
245
+ f"unset {selection.selection_source} (current_profile {current.alias} 사용)",
246
+ f"kiwoomcli auth switch {current.alias} (현재 유효 대상을 기본값으로 굳히기)",
247
+ ],
248
+ )
249
+ )
250
+
251
+
252
+ def _diagnose_install(findings: list[_Finding]) -> None:
253
+ entries = path_kiwoom_entries()
254
+ if len(entries) > 1:
255
+ finding = _Finding(
256
+ f"PATH 에 kiwoom 실행 파일이 {len(entries)}개 있어 어떤 것이 실행될지 모호합니다.",
257
+ ["which -a kiwoom 로 우선순위를 확인하세요"],
258
+ )
259
+ finding.fixes.extend(f"- {entry}" for entry in entries)
260
+ findings.append(finding)
261
+
262
+
263
+ def _print_detail() -> None:
264
+ current = _safe_current_profile(None)
265
+ print("상세(근거)")
266
+ print(" 선택 규칙 입력")
267
+ print(f" current_profile: {current.alias if current else '없음'}")
268
+ print(f" KIWOOM_MODE: {get_mode_from_env() or '없음'}")
269
+ print(f" KIWOOM_PROFILE: {get_profile_from_env() or '없음'}")
270
+ print(" 저장된 프로필")
271
+ try:
272
+ profiles = load_profiles()
273
+ except KiwoomError as exc:
274
+ print(f" 프로필을 읽을 수 없습니다: {exc}")
275
+ return
276
+ if not profiles:
277
+ print(" 저장된 계좌 별칭이 없습니다.")
278
+ return
279
+ for alias in sorted(profiles):
280
+ try:
281
+ # Pass the profile's own mode so ambient KIWOOM_MODE can't trigger
282
+ # a mode-mismatch while enumerating stored profiles.
283
+ context = build_auth_context(mode=profiles[alias].mode, profile=alias)
284
+ except KiwoomError as exc:
285
+ print(f" {alias}: 상태 확인 실패 ({exc})")
286
+ continue
287
+ status = context.status
288
+ token_label = "사용 가능" if status.token_reusable else ("있음" if status.has_token else "없음")
289
+ marker = " (current)" if current is not None and current.alias == alias else ""
290
+ print(f" {alias}{marker}: 서버={status.mode} 키/시크릿={'있음' if status.has_credentials else '없음'} "
291
+ f"토큰={token_label} 호출={'가능' if context.can_call_now else '불가'} "
292
+ f"재발급={'가능' if context.can_refresh else '불가'}")
293
+
294
+
295
+ def _print_environment_summary() -> None:
296
+ print("실행 환경")
297
+ print(f" 운영체제: {os_label()}")
298
+ tools = onboarding_tool_report()
299
+ if tools:
300
+ rendered = " · ".join(
301
+ f"{tool} {'있음' if found else '없음'}" for tool, found in tools
302
+ )
303
+ print(f" 도구: {rendered}")
304
+ print(f" 실행 대상: {resolved_current_executable()}")
305
+ print(f" 설정 파일: {settings_path()}")
306
+ print(f" 토큰 캐시: {cache_dir()}")
307
+ # Detailed PATH/package listing only matters when there is an anomaly; the
308
+ # multiple-kiwoom case is already surfaced as a finding above.
309
+ entries = path_kiwoom_entries()
310
+ if len(entries) > 1:
311
+ print(f" PATH 의 kiwoom: {len(entries)}개")
312
+ for entry in entries:
313
+ print(f" - {entry}")
314
+
315
+
316
+ def _safe_current_profile(findings: list[_Finding] | None):
317
+ try:
318
+ return get_current_profile()
319
+ except KiwoomError as exc:
320
+ if findings is not None:
321
+ findings.append(
322
+ _Finding(f"current_profile 을 읽을 수 없습니다: {exc}", ["settings.json 확인 또는 kiwoomcli setup"])
323
+ )
324
+ return None
kiwoom_cli/errors.py ADDED
@@ -0,0 +1,24 @@
1
+ """CLI-owned exception taxonomy.
2
+
3
+ Separates user-fixable input/policy violations from should-be-unreachable
4
+ internal assertions so ``main()`` can present each differently: input errors
5
+ get a single Korean line the user can act on, while internal errors surface as
6
+ "내부 오류" with a traceback instead of being mistaken for user mistakes.
7
+
8
+ API/server errors stay as ``kiwoom.core`` ``KiwoomError`` subclasses and are
9
+ passed through unchanged; they are intentionally NOT remapped here.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+
15
+ class CliError(Exception):
16
+ """Base class for errors owned by the CLI layer."""
17
+
18
+
19
+ class CliInputError(CliError):
20
+ """User input or policy violation (a single Korean message the user can fix)."""
21
+
22
+
23
+ class CliInternalError(CliError):
24
+ """Should-be-unreachable internal assertion (reported as 내부 오류)."""
@@ -0,0 +1,33 @@
1
+ """Command execution through the package runtime facade.
2
+
3
+ Public API for command modules; implementations live in the sibling modules
4
+ (``rest``, ``websocket``, ``condition``, ``waits``).
5
+ """
6
+
7
+ from kiwoom_cli.executor.condition import (
8
+ DEFAULT_CONDITION_MAX_WAIT_SECONDS,
9
+ execute_condition_command,
10
+ )
11
+ from kiwoom_cli.executor.rest import (
12
+ execute_rest_command,
13
+ preview_rest_command,
14
+ validate_preview_request,
15
+ validate_request_body,
16
+ )
17
+ from kiwoom_cli.executor.websocket import (
18
+ DEFAULT_STREAM_MAX_WAIT_SECONDS,
19
+ execute_websocket_request_once,
20
+ execute_websocket_stream,
21
+ )
22
+
23
+ __all__ = [
24
+ "DEFAULT_CONDITION_MAX_WAIT_SECONDS",
25
+ "DEFAULT_STREAM_MAX_WAIT_SECONDS",
26
+ "execute_condition_command",
27
+ "execute_rest_command",
28
+ "execute_websocket_request_once",
29
+ "execute_websocket_stream",
30
+ "preview_rest_command",
31
+ "validate_preview_request",
32
+ "validate_request_body",
33
+ ]