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
@@ -0,0 +1,147 @@
1
+ """Shared argparse helpers for resource commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ from datetime import datetime
7
+ import re
8
+
9
+ from kiwoom.core.types import VALID_MODES
10
+ from kiwoom_cli.errors import CliInputError
11
+ from kiwoom_cli.output import OUTPUT_FORMATS
12
+
13
+
14
+ STOCK_CODE_PATTERN = re.compile(r"^\d{6}$")
15
+ EXCHANGE_STOCK_CODE_PATTERN = re.compile(r"^\d{6}(?:_(?:NX|AL))?$")
16
+ INSTRUMENT_CODE_PATTERN = re.compile(r"^[A-Za-z0-9]{6}$")
17
+ SECTOR_CODE_PATTERN = re.compile(r"^\d{3}$")
18
+ DATE_YYYYMMDD_PATTERN = re.compile(r"^\d{8}$")
19
+ INTEGER_PATTERN = re.compile(r"^\d+$")
20
+ ORDER_ID_PATTERN = re.compile(r"^\d{7}$")
21
+ PREVIEW_ORDER_ID_PATTERN = re.compile(r"^\d+$")
22
+
23
+
24
+ def add_runtime_args(parser: argparse.ArgumentParser) -> None:
25
+ parser.add_argument(
26
+ "--profile",
27
+ help="사용할 계좌 별칭입니다. --mode/KIWOOM_MODE가 없을 때 생략하면 현재 계좌 별칭을 사용합니다.",
28
+ )
29
+ parser.add_argument("--mode", choices=VALID_MODES, help="계좌 별칭 대신 모드를 직접 지정합니다.")
30
+
31
+
32
+ def add_output_format_arg(parser: argparse.ArgumentParser) -> None:
33
+ parser.add_argument("--format", choices=OUTPUT_FORMATS, default="pretty", help="출력 형식")
34
+
35
+
36
+ def resolve_optional_alias(
37
+ positional: str | None, option: str | None, option_name: str
38
+ ) -> str | None:
39
+ if positional and option and positional != option:
40
+ raise CliInputError(
41
+ "계좌 별칭은 positional 또는 "
42
+ f"{option_name} 중 하나만 지정해 주세요: {positional!r} != {option!r}"
43
+ )
44
+ return option or positional
45
+
46
+
47
+ def stock_code(value: str) -> str:
48
+ if not STOCK_CODE_PATTERN.fullmatch(value):
49
+ raise argparse.ArgumentTypeError("--code는 6자리 국내 종목코드여야 합니다.")
50
+ return value
51
+
52
+
53
+ def exchange_stock_code(value: str) -> str:
54
+ normalized = value.upper()
55
+ if not EXCHANGE_STOCK_CODE_PATTERN.fullmatch(normalized):
56
+ raise argparse.ArgumentTypeError(
57
+ "--code는 6자리 종목코드이거나 _NX/_AL 접미사가 붙은 거래소별 종목코드여야 합니다."
58
+ )
59
+ return normalized
60
+
61
+
62
+ def instrument_code(value: str) -> str:
63
+ if not INSTRUMENT_CODE_PATTERN.fullmatch(value):
64
+ raise argparse.ArgumentTypeError("--code는 6자리 영문/숫자 종목코드여야 합니다.")
65
+ return value.upper()
66
+
67
+
68
+ def sector_code(value: str) -> str:
69
+ if not SECTOR_CODE_PATTERN.fullmatch(value):
70
+ raise argparse.ArgumentTypeError("--code는 3자리 키움 업종코드여야 합니다.")
71
+ return value
72
+
73
+
74
+ def date_yyyymmdd(value: str) -> str:
75
+ if not DATE_YYYYMMDD_PATTERN.fullmatch(value):
76
+ raise argparse.ArgumentTypeError("날짜는 YYYYMMDD 형식이어야 합니다.")
77
+ try:
78
+ datetime.strptime(value, "%Y%m%d")
79
+ except ValueError as exc:
80
+ raise argparse.ArgumentTypeError("날짜가 실제 달력 날짜가 아닙니다.") from exc
81
+ return value
82
+
83
+
84
+ def adjusted_price_flag(value: str) -> str:
85
+ normalized = value.strip().lower()
86
+ if normalized in {"1", "true", "yes", "y", "adjusted"}:
87
+ return "1"
88
+ if normalized in {"0", "false", "no", "n", "raw", "unadjusted"}:
89
+ return "0"
90
+ raise argparse.ArgumentTypeError(
91
+ "--adjusted 값은 0, 1, true, false, adjusted, raw 중 하나여야 합니다."
92
+ )
93
+
94
+
95
+ def positive_int_string(value: str) -> str:
96
+ if not INTEGER_PATTERN.fullmatch(value):
97
+ raise argparse.ArgumentTypeError("값은 양의 정수여야 합니다.")
98
+ if int(value) <= 0:
99
+ raise argparse.ArgumentTypeError("값은 0보다 커야 합니다.")
100
+ return value
101
+
102
+
103
+ def nonnegative_int_string(value: str) -> str:
104
+ if not INTEGER_PATTERN.fullmatch(value):
105
+ raise argparse.ArgumentTypeError("값은 0 이상의 정수여야 합니다.")
106
+ if int(value) < 0:
107
+ raise argparse.ArgumentTypeError("값은 0 이상이어야 합니다.")
108
+ return value
109
+
110
+
111
+ def price_string(value: str) -> str:
112
+ if not INTEGER_PATTERN.fullmatch(value):
113
+ raise argparse.ArgumentTypeError("가격은 정수 문자열이어야 합니다.")
114
+ if int(value) < 0:
115
+ raise argparse.ArgumentTypeError("가격은 0 이상이어야 합니다.")
116
+ return value
117
+
118
+
119
+ def order_id(value: str) -> str:
120
+ if not ORDER_ID_PATTERN.fullmatch(value):
121
+ raise argparse.ArgumentTypeError("주문번호는 키움 7자리 주문번호여야 합니다.")
122
+ return value
123
+
124
+
125
+ def preview_order_id(value: str) -> str:
126
+ if not PREVIEW_ORDER_ID_PATTERN.fullmatch(value):
127
+ raise argparse.ArgumentTypeError("주문번호는 숫자만 입력할 수 있습니다.")
128
+ return value
129
+
130
+
131
+ def page_count(value: str) -> int:
132
+ try:
133
+ parsed = int(value)
134
+ except ValueError as exc:
135
+ raise argparse.ArgumentTypeError("페이지 수는 정수여야 합니다.") from exc
136
+ if parsed < 0:
137
+ raise argparse.ArgumentTypeError("페이지 수는 0(전체) 이상이어야 합니다.")
138
+ return parsed
139
+
140
+
141
+ def add_pages_arg(parser: argparse.ArgumentParser) -> None:
142
+ parser.add_argument(
143
+ "--pages",
144
+ type=page_count,
145
+ default=1,
146
+ help="연속조회 페이지 수입니다 (기본 1, 0은 서버가 제공하는 모든 페이지).",
147
+ )
@@ -0,0 +1,64 @@
1
+ """Shared auth-context composition for CLI status/doctor/error rendering.
2
+
3
+ This module does not redefine auth state. It composes the two existing
4
+ sources of truth:
5
+
6
+ - `kiwoom.core.runtime.describe_selection` — which input won target selection.
7
+ - `kiwoom.core.auth.AuthStatus` — credential/token health for that target.
8
+
9
+ `auth status`, `auth list`, `doctor`, and credential error messages all build
10
+ on `AuthContext` so they cannot disagree about "can I call now / later?".
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from dataclasses import dataclass
16
+
17
+ from kiwoom.core.auth import AuthStatus
18
+ from kiwoom.core.runtime import SelectionContext, describe_selection, get_auth
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class AuthContext:
23
+ selection: SelectionContext
24
+ status: AuthStatus
25
+
26
+ @property
27
+ def can_call_now(self) -> bool:
28
+ """A request can be served right now."""
29
+ # A reusable cached token works even without stored credentials;
30
+ # otherwise credentials let auth issue/refresh before the request.
31
+ return self.status.token_reusable or self.status.has_credentials
32
+
33
+ @property
34
+ def can_refresh(self) -> bool:
35
+ """A fresh token can be issued/refreshed (needs credentials)."""
36
+ return self.status.has_credentials
37
+
38
+ def warnings(self) -> list[str]:
39
+ messages: list[str] = []
40
+ if self.status.token_warning:
41
+ messages.append(self.status.token_warning)
42
+ if self.status.token_reusable and not self.status.has_credentials:
43
+ messages.append(
44
+ "지금은 캐시 토큰으로 호출할 수 있지만, 토큰 만료 후에는 자동 재발급할 수 없습니다."
45
+ )
46
+ if not self.can_call_now:
47
+ messages.append("현재 이 대상으로는 API를 호출할 수 없습니다.")
48
+ return messages
49
+
50
+
51
+ def build_auth_context(
52
+ mode: str | None = None,
53
+ *,
54
+ profile: str | None = None,
55
+ ) -> AuthContext:
56
+ """Resolve selection then snapshot auth health for that target.
57
+
58
+ Raises `ModeNotConfiguredError` when nothing selects a target, matching
59
+ `describe_selection`/`get_auth`.
60
+ """
61
+
62
+ selection = describe_selection(mode, profile=profile)
63
+ auth = get_auth(mode, profile=profile)
64
+ return AuthContext(selection=selection, status=auth.status())
kiwoom_cli/banner.py ADDED
@@ -0,0 +1,125 @@
1
+ """`kiwoomcli setup` 시작 시 표시하는 ASCII 배너.
2
+
3
+ 컬러: KIWOOM 워드마크는 네이비, 오른쪽 'K' 로고는 기본 회색(#BEBEBE)에
4
+ 상단 화살표만 마젠타 핑크로 포인트. 자막/버전은 회색. 색은 대화형 터미널에서만
5
+ 적용하고, 파이프/리다이렉트(비-TTY)나 NO_COLOR 환경에서는 무채색으로 출력한다.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import ctypes
11
+ import os
12
+ import sys
13
+ from importlib.metadata import PackageNotFoundError, version
14
+
15
+ # 키움 브랜드 컬러 (truecolor).
16
+ _NAVY = "\033[1;38;2;26;31;113m" # KIWOOM 워드마크
17
+ _GRAY = "\033[1;38;2;190;190;190m" # 오른쪽 K 기본색 #BEBEBE
18
+ _MAGENTA = "\033[1;38;2;230;0;122m" # K 상단 화살표 포인트
19
+ _WHITE = "\033[1;38;2;255;255;255m" # 자막 / 버전
20
+ _RESET = "\033[0m"
21
+
22
+ # 워드마크(좌) / 오른쪽 K(우) 경계 컬럼.
23
+ _SPLIT = 51
24
+ # 오른쪽 K에서 상단 화살표(마젠타) 행. 하단(4~6행)은 회색.
25
+ _ARROW_ROWS = {0, 1, 2, 3}
26
+
27
+ _ART = """\
28
+ ██╗ ██╗██╗██╗ ██╗ ██████╗ ██████╗ ███╗ ███╗ ██ ▜██
29
+ ██║ ██╔╝██║██║ ██║██╔═══██╗██╔═══██╗████╗ ████║ ██ ╱█▛▜
30
+ █████╔╝ ██║██║ █╗ ██║██║ ██║██║ ██║██╔████╔██║ ██ ███╱
31
+ ██╔═██╗ ██║██║███╗██║██║ ██║██║ ██║██║╚██╔╝██║ █████╱
32
+ ██║ ██╗██║╚███╔███╔╝╚██████╔╝╚██████╔╝██║ ╚═╝ ██║ ██ ╲██╲
33
+ ╚═╝ ╚═╝╚═╝ ╚══╝╚══╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝ ██ ╲██╲
34
+ ██ ╲██▙"""
35
+
36
+ _SUBTITLE = " 키움증권 CLI"
37
+
38
+
39
+ def _use_color() -> bool:
40
+ if "NO_COLOR" in os.environ:
41
+ return False
42
+ if not sys.stdout.isatty():
43
+ return False
44
+ if os.environ.get("TERM") == "dumb":
45
+ return False
46
+ if os.name != "nt":
47
+ return True
48
+ if os.environ.get("WT_SESSION") or os.environ.get("ANSICON"):
49
+ return True
50
+ if os.environ.get("ConEmuANSI", "").upper() == "ON":
51
+ return True
52
+ return _enable_windows_vt()
53
+
54
+
55
+ def _enable_windows_vt() -> bool:
56
+ std_output_handle = -11
57
+ enable_virtual_terminal_processing = 0x0004
58
+ try:
59
+ kernel32 = ctypes.windll.kernel32
60
+ handle = kernel32.GetStdHandle(std_output_handle)
61
+ if handle in (-1, 0):
62
+ return False
63
+ mode = ctypes.c_uint()
64
+ if not kernel32.GetConsoleMode(handle, ctypes.byref(mode)):
65
+ return False
66
+ if mode.value & enable_virtual_terminal_processing:
67
+ return True
68
+ return bool(
69
+ kernel32.SetConsoleMode(
70
+ handle,
71
+ mode.value | enable_virtual_terminal_processing,
72
+ )
73
+ )
74
+ except Exception:
75
+ return False
76
+
77
+
78
+ _BOX = set("╗╝═║╔╚") # 워드마크 외곽선(그림자) 문자
79
+
80
+
81
+ def _colorize_wordmark(segment: str) -> str:
82
+ """워드마크: `█` 블록은 네이비, 외곽선 문자는 #BEBEBE로 대비를 준다."""
83
+ out: list[str] = []
84
+ current: str | None = None
85
+ for char in segment:
86
+ color = _GRAY if char in _BOX else _NAVY
87
+ if color != current:
88
+ out.append(color)
89
+ current = color
90
+ out.append(char)
91
+ return "".join(out)
92
+
93
+
94
+ def _arrow_start(right: str) -> int:
95
+ """오른쪽 조각에서 마지막 공백 뒤(=상단 화살표 시작) 인덱스."""
96
+ index = len(right)
97
+ while index > 0 and right[index - 1] != " ":
98
+ index -= 1
99
+ return index
100
+
101
+
102
+ def print_banner() -> None:
103
+ try:
104
+ current = version("kiwoomcli")
105
+ except PackageNotFoundError:
106
+ current = "dev"
107
+ ver_line = f" ver {current}"
108
+
109
+ if _use_color():
110
+ for row, line in enumerate(_ART.splitlines()):
111
+ left, right = line[:_SPLIT], line[_SPLIT:]
112
+ if row in _ARROW_ROWS:
113
+ cut = _arrow_start(right)
114
+ if row == 3:
115
+ cut += 1 # 엘보 맨 왼쪽 한 칸은 기둥이므로 회색 유지
116
+ print(f"{_colorize_wordmark(left)}{_GRAY}{right[:cut]}{_MAGENTA}{right[cut:]}{_RESET}")
117
+ else:
118
+ print(f"{_colorize_wordmark(left)}{_GRAY}{right}{_RESET}")
119
+ print(f"{_WHITE}{_SUBTITLE}{_RESET}")
120
+ print(f"{_WHITE}{ver_line}{_RESET}")
121
+ else:
122
+ print(_ART)
123
+ print(_SUBTITLE)
124
+ print(ver_line)
125
+ print()
@@ -0,0 +1 @@
1
+ """Domain command handlers for the installed kiwoom CLI."""