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
kiwoom_cli/arguments.py
ADDED
|
@@ -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."""
|