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/__init__.py
ADDED
kiwoom_cli/__main__.py
ADDED
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
"""Load explicit CLI argument-to-Kiwoom-field mappings."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import csv
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from functools import lru_cache
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Callable
|
|
11
|
+
|
|
12
|
+
from kiwoom_cli.errors import CliInputError, CliInternalError
|
|
13
|
+
|
|
14
|
+
from kiwoom_cli.arguments import (
|
|
15
|
+
adjusted_price_flag,
|
|
16
|
+
date_yyyymmdd,
|
|
17
|
+
exchange_stock_code,
|
|
18
|
+
instrument_code,
|
|
19
|
+
nonnegative_int_string,
|
|
20
|
+
order_id,
|
|
21
|
+
preview_order_id,
|
|
22
|
+
positive_int_string,
|
|
23
|
+
price_string,
|
|
24
|
+
sector_code,
|
|
25
|
+
stock_code,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
MAP_PATH = Path(__file__).resolve().parent / "maps" / "arguments.csv"
|
|
30
|
+
ORDER_PRICE_POLICY_PATH = (
|
|
31
|
+
Path(__file__).resolve().parent / "maps" / "order_price_policies.csv"
|
|
32
|
+
)
|
|
33
|
+
ALLOWED_ORDER_PRICE_POLICIES = {"required", "optional", "forbidden"}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
TYPE_PARSERS: dict[str, Callable[[str], str]] = {
|
|
37
|
+
"stock_code": stock_code,
|
|
38
|
+
"exchange_stock_code": exchange_stock_code,
|
|
39
|
+
"instrument_code": instrument_code,
|
|
40
|
+
"sector_code": sector_code,
|
|
41
|
+
"date_yyyymmdd": date_yyyymmdd,
|
|
42
|
+
"adjusted_price_flag": adjusted_price_flag,
|
|
43
|
+
"quantity": positive_int_string,
|
|
44
|
+
"cancel_quantity": nonnegative_int_string,
|
|
45
|
+
"price": price_string,
|
|
46
|
+
"order_id": order_id,
|
|
47
|
+
"preview_order_id": preview_order_id,
|
|
48
|
+
"order_type": str,
|
|
49
|
+
"market": str,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class OrderPricePolicy:
|
|
55
|
+
command_path: str
|
|
56
|
+
order_type: str
|
|
57
|
+
price_option: str
|
|
58
|
+
price_policy: str
|
|
59
|
+
condition_price_option: str
|
|
60
|
+
condition_price_policy: str
|
|
61
|
+
description: str
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class ArgumentDefinition:
|
|
66
|
+
command_path: str
|
|
67
|
+
option: str
|
|
68
|
+
dest: str
|
|
69
|
+
kiwoom_field: str
|
|
70
|
+
required: bool
|
|
71
|
+
type_name: str
|
|
72
|
+
choices: tuple[str, ...]
|
|
73
|
+
value_map: dict[str, str]
|
|
74
|
+
default: str
|
|
75
|
+
description: str
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def load_argument_definitions(map_path: Path | None = None) -> list[ArgumentDefinition]:
|
|
79
|
+
resolved_path = map_path or MAP_PATH
|
|
80
|
+
if map_path is None:
|
|
81
|
+
return list(_load_default_argument_definitions())
|
|
82
|
+
return _load_argument_definitions_from_path(resolved_path)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@lru_cache(maxsize=1)
|
|
86
|
+
def _load_default_argument_definitions() -> tuple[ArgumentDefinition, ...]:
|
|
87
|
+
return tuple(_load_argument_definitions_from_path(MAP_PATH))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _load_argument_definitions_from_path(resolved_path: Path) -> list[ArgumentDefinition]:
|
|
91
|
+
with resolved_path.open(encoding="utf-8", newline="") as file:
|
|
92
|
+
return [_definition_from_row(row) for row in csv.DictReader(file)]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def get_argument_definitions(command_path: str) -> list[ArgumentDefinition]:
|
|
96
|
+
return [
|
|
97
|
+
definition
|
|
98
|
+
for definition in load_argument_definitions()
|
|
99
|
+
if definition.command_path == command_path
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def load_order_price_policies(
|
|
104
|
+
map_path: Path | None = None,
|
|
105
|
+
) -> list[OrderPricePolicy]:
|
|
106
|
+
resolved_path = map_path or ORDER_PRICE_POLICY_PATH
|
|
107
|
+
if not resolved_path.exists():
|
|
108
|
+
return []
|
|
109
|
+
with resolved_path.open(encoding="utf-8", newline="") as file:
|
|
110
|
+
return [_order_price_policy_from_row(row) for row in csv.DictReader(file)]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def get_order_price_policy(
|
|
114
|
+
command_path: str, order_type: str
|
|
115
|
+
) -> OrderPricePolicy | None:
|
|
116
|
+
for policy in load_order_price_policies():
|
|
117
|
+
if policy.command_path == command_path and policy.order_type == order_type:
|
|
118
|
+
return policy
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def add_mapped_arguments(
|
|
123
|
+
parser: argparse.ArgumentParser,
|
|
124
|
+
command_path: str,
|
|
125
|
+
*,
|
|
126
|
+
required_overrides: dict[str, bool] | None = None,
|
|
127
|
+
action_overrides: dict[str, str] | None = None,
|
|
128
|
+
) -> None:
|
|
129
|
+
required_overrides = required_overrides or {}
|
|
130
|
+
action_overrides = action_overrides or {}
|
|
131
|
+
definitions = get_argument_definitions(command_path)
|
|
132
|
+
alternative_fields = _alternative_required_fields(definitions)
|
|
133
|
+
for definition in definitions:
|
|
134
|
+
if not definition.option:
|
|
135
|
+
continue
|
|
136
|
+
required = required_overrides.get(
|
|
137
|
+
definition.dest, definition.required and definition.default == ""
|
|
138
|
+
)
|
|
139
|
+
if definition.kiwoom_field in alternative_fields:
|
|
140
|
+
required = False
|
|
141
|
+
kwargs: dict[str, object] = {
|
|
142
|
+
"dest": definition.dest,
|
|
143
|
+
"required": required,
|
|
144
|
+
"help": definition.description,
|
|
145
|
+
}
|
|
146
|
+
action = action_overrides.get(definition.dest)
|
|
147
|
+
if action:
|
|
148
|
+
kwargs["action"] = action
|
|
149
|
+
type_parser = TYPE_PARSERS.get(definition.type_name)
|
|
150
|
+
if type_parser is not None:
|
|
151
|
+
kwargs["type"] = type_parser
|
|
152
|
+
if definition.choices:
|
|
153
|
+
kwargs["choices"] = definition.choices
|
|
154
|
+
if definition.default and not action:
|
|
155
|
+
kwargs["default"] = definition.default
|
|
156
|
+
parser.add_argument(definition.option, **kwargs)
|
|
157
|
+
attach_mapped_help_epilog(parser, command_path, definitions)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def attach_mapped_help_epilog(
|
|
161
|
+
parser: argparse.ArgumentParser,
|
|
162
|
+
command_path: str,
|
|
163
|
+
definitions: list[ArgumentDefinition] | None = None,
|
|
164
|
+
) -> None:
|
|
165
|
+
definitions = definitions if definitions is not None else get_argument_definitions(command_path)
|
|
166
|
+
try:
|
|
167
|
+
from kiwoom_cli.registry import get_command_by_path
|
|
168
|
+
|
|
169
|
+
command = get_command_by_path(command_path)
|
|
170
|
+
except ValueError:
|
|
171
|
+
command = None
|
|
172
|
+
|
|
173
|
+
sections: list[str] = []
|
|
174
|
+
if command is not None:
|
|
175
|
+
sections.extend(
|
|
176
|
+
[
|
|
177
|
+
"Summary:",
|
|
178
|
+
f" {command.api_name}.",
|
|
179
|
+
"",
|
|
180
|
+
"Behavior:",
|
|
181
|
+
f" {_behavior_text(command.command_path, command.safety_policy, command.coverage_status)}",
|
|
182
|
+
"",
|
|
183
|
+
"Examples:",
|
|
184
|
+
f" {_example_command(command_path, definitions)}",
|
|
185
|
+
"",
|
|
186
|
+
"OpenAPI mapping:",
|
|
187
|
+
f" API ID: {command.api_id}",
|
|
188
|
+
f" Request: {command.method} {command.path}",
|
|
189
|
+
]
|
|
190
|
+
)
|
|
191
|
+
else:
|
|
192
|
+
sections.extend(
|
|
193
|
+
[
|
|
194
|
+
"Examples:",
|
|
195
|
+
f" {_example_command(command_path, definitions)}",
|
|
196
|
+
"",
|
|
197
|
+
"OpenAPI mapping:",
|
|
198
|
+
]
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
for line in _mapping_lines(definitions):
|
|
202
|
+
sections.append(f" {line}")
|
|
203
|
+
|
|
204
|
+
extra = "\n".join(sections).rstrip()
|
|
205
|
+
if parser.epilog:
|
|
206
|
+
parser.epilog = f"{parser.epilog.rstrip()}\n\n{extra}"
|
|
207
|
+
else:
|
|
208
|
+
parser.epilog = extra
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _behavior_text(command_path: str, safety_policy: str, coverage_status: str) -> str:
|
|
212
|
+
if _is_stream_subscription_command(command_path):
|
|
213
|
+
return (
|
|
214
|
+
"WebSocket stream 구독 명령입니다. foreground에서 실시간 이벤트를 수신하며, "
|
|
215
|
+
"--count/--duration/--check로 종료 조건을 주거나 --watch로 계속 수신합니다. "
|
|
216
|
+
"--output으로 이벤트를 JSONL 파일에 기록할 수 있습니다."
|
|
217
|
+
)
|
|
218
|
+
if safety_policy == "read":
|
|
219
|
+
return "조회 전용 명령입니다. 계좌/주문 상태를 변경하지 않습니다."
|
|
220
|
+
if safety_policy == "account_read":
|
|
221
|
+
return "계좌 조회 명령입니다. 출력에는 공통 계좌번호 redaction 정책이 적용됩니다."
|
|
222
|
+
if safety_policy == "order_write":
|
|
223
|
+
if coverage_status == "preview-only":
|
|
224
|
+
return (
|
|
225
|
+
"preview-only 명령입니다. --confirm이 있어도 실제 주문 전송은 차단되고 "
|
|
226
|
+
"미전송 주문 확인만 출력합니다."
|
|
227
|
+
)
|
|
228
|
+
return "--confirm 없이는 실제 주문 API를 호출하지 않고 미전송 주문 확인만 출력합니다."
|
|
229
|
+
if coverage_status == "preview-only":
|
|
230
|
+
return "요청 생성/검증 전용 명령입니다. 실제 쓰기 요청은 전송하지 않습니다."
|
|
231
|
+
if safety_policy == "review_required":
|
|
232
|
+
return "실행 전 정책 검토가 필요한 명령입니다."
|
|
233
|
+
if safety_policy == "auth_write":
|
|
234
|
+
return "인증/토큰 상태를 변경할 수 있는 명령입니다."
|
|
235
|
+
return f"안전 정책: {safety_policy}, 적용 상태: {coverage_status}."
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _is_stream_subscription_command(command_path: str) -> bool:
|
|
239
|
+
prefix = "kiwoomcli domestic streams "
|
|
240
|
+
if command_path.startswith(prefix):
|
|
241
|
+
command = command_path.removeprefix(prefix)
|
|
242
|
+
return not command.startswith("conditions-")
|
|
243
|
+
return False
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _example_command(command_path: str, definitions: list[ArgumentDefinition]) -> str:
|
|
247
|
+
parts = [command_path]
|
|
248
|
+
included_options: set[str] = set()
|
|
249
|
+
for definition in definitions:
|
|
250
|
+
if not definition.option:
|
|
251
|
+
continue
|
|
252
|
+
if not definition.required or definition.default:
|
|
253
|
+
continue
|
|
254
|
+
parts.extend([definition.option, _example_value(definition)])
|
|
255
|
+
included_options.add(definition.option)
|
|
256
|
+
_append_order_policy_example_options(parts, included_options, command_path, definitions)
|
|
257
|
+
return " ".join(parts)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _append_order_policy_example_options(
|
|
261
|
+
parts: list[str],
|
|
262
|
+
included_options: set[str],
|
|
263
|
+
command_path: str,
|
|
264
|
+
definitions: list[ArgumentDefinition],
|
|
265
|
+
) -> None:
|
|
266
|
+
order_type = next(
|
|
267
|
+
(
|
|
268
|
+
definition.choices[0]
|
|
269
|
+
for definition in definitions
|
|
270
|
+
if definition.dest == "order_type" and definition.choices
|
|
271
|
+
),
|
|
272
|
+
"",
|
|
273
|
+
)
|
|
274
|
+
if not order_type:
|
|
275
|
+
return
|
|
276
|
+
policy = get_order_price_policy(command_path, order_type)
|
|
277
|
+
if policy is None:
|
|
278
|
+
return
|
|
279
|
+
definitions_by_option = {
|
|
280
|
+
definition.option: definition for definition in definitions if definition.option
|
|
281
|
+
}
|
|
282
|
+
for option, option_policy in (
|
|
283
|
+
(policy.price_option, policy.price_policy),
|
|
284
|
+
(policy.condition_price_option, policy.condition_price_policy),
|
|
285
|
+
):
|
|
286
|
+
if option_policy != "required" or not option or option in included_options:
|
|
287
|
+
continue
|
|
288
|
+
definition = definitions_by_option.get(option)
|
|
289
|
+
if definition is None:
|
|
290
|
+
continue
|
|
291
|
+
parts.extend([option, _example_value(definition)])
|
|
292
|
+
included_options.add(option)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _example_value(definition: ArgumentDefinition) -> str:
|
|
296
|
+
if definition.choices:
|
|
297
|
+
return definition.choices[0]
|
|
298
|
+
if definition.type_name in {"stock_code", "exchange_stock_code"}:
|
|
299
|
+
return "005930"
|
|
300
|
+
if definition.type_name == "sector_code":
|
|
301
|
+
return "001"
|
|
302
|
+
if definition.type_name == "instrument_code":
|
|
303
|
+
return "M04020"
|
|
304
|
+
if definition.type_name == "date_yyyymmdd":
|
|
305
|
+
return "20260626"
|
|
306
|
+
if definition.type_name in {"quantity", "cancel_quantity"}:
|
|
307
|
+
return "10"
|
|
308
|
+
if definition.type_name == "price":
|
|
309
|
+
return "70000"
|
|
310
|
+
if definition.type_name in {"order_id", "preview_order_id"}:
|
|
311
|
+
return "1234567"
|
|
312
|
+
return f"<{definition.dest or 'value'}>"
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _mapping_lines(definitions: list[ArgumentDefinition]) -> list[str]:
|
|
316
|
+
lines: list[str] = []
|
|
317
|
+
for definition in definitions:
|
|
318
|
+
if not definition.kiwoom_field:
|
|
319
|
+
continue
|
|
320
|
+
option = definition.option or "(internal)"
|
|
321
|
+
status = "required" if definition.required and not definition.default else "optional"
|
|
322
|
+
suffix = f", default: {definition.default}" if definition.default else ""
|
|
323
|
+
lines.append(f"{option} -> {definition.kiwoom_field} ({status}{suffix})")
|
|
324
|
+
if definition.value_map:
|
|
325
|
+
for choice, raw_value in definition.value_map.items():
|
|
326
|
+
lines.append(f" {choice} = {raw_value}")
|
|
327
|
+
return lines
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def build_body(command_path: str, args: argparse.Namespace) -> dict[str, str]:
|
|
331
|
+
definitions = get_argument_definitions(command_path)
|
|
332
|
+
validate_order_price_policy(command_path, args, definitions)
|
|
333
|
+
body: dict[str, str] = {}
|
|
334
|
+
definitions_by_field = _definitions_by_field(definitions)
|
|
335
|
+
handled_fields: set[str] = set()
|
|
336
|
+
for definition in definitions:
|
|
337
|
+
field = definition.kiwoom_field
|
|
338
|
+
if not field or field in handled_fields:
|
|
339
|
+
continue
|
|
340
|
+
handled_fields.add(field)
|
|
341
|
+
field_definitions = definitions_by_field[field]
|
|
342
|
+
if len(field_definitions) == 1:
|
|
343
|
+
resolved = _resolve_definition_value(command_path, field_definitions[0], args)
|
|
344
|
+
else:
|
|
345
|
+
resolved = _resolve_alternative_value(command_path, field, field_definitions, args)
|
|
346
|
+
if resolved is None:
|
|
347
|
+
continue
|
|
348
|
+
selected_definition, value = resolved
|
|
349
|
+
if isinstance(value, list):
|
|
350
|
+
body[field] = ",".join(
|
|
351
|
+
_map_value(selected_definition, str(item)) for item in value
|
|
352
|
+
)
|
|
353
|
+
else:
|
|
354
|
+
body[field] = _map_value(selected_definition, str(value))
|
|
355
|
+
return body
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _definitions_by_field(
|
|
359
|
+
definitions: list[ArgumentDefinition],
|
|
360
|
+
) -> dict[str, list[ArgumentDefinition]]:
|
|
361
|
+
grouped: dict[str, list[ArgumentDefinition]] = {}
|
|
362
|
+
for definition in definitions:
|
|
363
|
+
if definition.kiwoom_field:
|
|
364
|
+
grouped.setdefault(definition.kiwoom_field, []).append(definition)
|
|
365
|
+
return grouped
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _alternative_required_fields(definitions: list[ArgumentDefinition]) -> set[str]:
|
|
369
|
+
return {
|
|
370
|
+
field
|
|
371
|
+
for field, field_definitions in _definitions_by_field(definitions).items()
|
|
372
|
+
if len(field_definitions) > 1
|
|
373
|
+
and any(definition.required for definition in field_definitions)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _resolve_definition_value(
|
|
378
|
+
command_path: str,
|
|
379
|
+
definition: ArgumentDefinition,
|
|
380
|
+
args: argparse.Namespace,
|
|
381
|
+
) -> tuple[ArgumentDefinition, object] | None:
|
|
382
|
+
value: object = definition.default
|
|
383
|
+
if definition.dest:
|
|
384
|
+
arg_value = getattr(args, definition.dest, None)
|
|
385
|
+
if arg_value not in (None, ""):
|
|
386
|
+
value = arg_value
|
|
387
|
+
if value == "":
|
|
388
|
+
if definition.required:
|
|
389
|
+
raise CliInputError(
|
|
390
|
+
f"{command_path}: 필수 옵션이 빠졌습니다: {definition.option}"
|
|
391
|
+
)
|
|
392
|
+
return None
|
|
393
|
+
return definition, value
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _resolve_alternative_value(
|
|
397
|
+
command_path: str,
|
|
398
|
+
field: str,
|
|
399
|
+
definitions: list[ArgumentDefinition],
|
|
400
|
+
args: argparse.Namespace,
|
|
401
|
+
) -> tuple[ArgumentDefinition, object] | None:
|
|
402
|
+
provided: list[tuple[ArgumentDefinition, object]] = []
|
|
403
|
+
defaults: list[tuple[ArgumentDefinition, object]] = []
|
|
404
|
+
for definition in definitions:
|
|
405
|
+
value: object = definition.default
|
|
406
|
+
if definition.dest:
|
|
407
|
+
arg_value = getattr(args, definition.dest, None)
|
|
408
|
+
if arg_value not in (None, ""):
|
|
409
|
+
provided.append((definition, arg_value))
|
|
410
|
+
continue
|
|
411
|
+
if value != "":
|
|
412
|
+
defaults.append((definition, value))
|
|
413
|
+
|
|
414
|
+
if len(provided) > 1:
|
|
415
|
+
options = ", ".join(definition.option for definition in definitions)
|
|
416
|
+
raise CliInputError(
|
|
417
|
+
f"{command_path}: {options} 옵션은 함께 사용할 수 없습니다."
|
|
418
|
+
)
|
|
419
|
+
if provided:
|
|
420
|
+
return provided[0]
|
|
421
|
+
if defaults:
|
|
422
|
+
return defaults[0]
|
|
423
|
+
if any(definition.required for definition in definitions):
|
|
424
|
+
options = ", ".join(definition.option for definition in definitions)
|
|
425
|
+
raise CliInputError(f"{command_path}: 다음 옵션 중 하나가 필요합니다: {options}")
|
|
426
|
+
return None
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _definition_from_row(row: dict[str, str]) -> ArgumentDefinition:
|
|
430
|
+
return ArgumentDefinition(
|
|
431
|
+
command_path=row["command_path"],
|
|
432
|
+
option=row["option"],
|
|
433
|
+
dest=row["dest"],
|
|
434
|
+
kiwoom_field=row["kiwoom_field"],
|
|
435
|
+
required=row["required"] == "Y",
|
|
436
|
+
type_name=row["type"],
|
|
437
|
+
choices=_split_choices(row["choices"]),
|
|
438
|
+
value_map=_split_value_map(row["value_map"]),
|
|
439
|
+
default=row["default"],
|
|
440
|
+
description=row["description"],
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _split_choices(value: str) -> tuple[str, ...]:
|
|
445
|
+
return tuple(part.strip() for part in value.split("|") if part.strip())
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _split_value_map(value: str) -> dict[str, str]:
|
|
449
|
+
mapping: dict[str, str] = {}
|
|
450
|
+
for part in value.split(";"):
|
|
451
|
+
if not part.strip():
|
|
452
|
+
continue
|
|
453
|
+
key, mapped_value = part.split("=", 1)
|
|
454
|
+
mapping[key.strip()] = mapped_value.strip()
|
|
455
|
+
return mapping
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _map_value(definition: ArgumentDefinition, value: str) -> str:
|
|
459
|
+
if not definition.value_map:
|
|
460
|
+
return value
|
|
461
|
+
try:
|
|
462
|
+
return definition.value_map[value]
|
|
463
|
+
except KeyError as exc:
|
|
464
|
+
allowed = ", ".join(sorted(definition.value_map))
|
|
465
|
+
raise CliInputError(
|
|
466
|
+
f"{definition.command_path}: {definition.option} 값은 다음 중 하나여야 합니다: {allowed}"
|
|
467
|
+
) from exc
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _order_price_policy_from_row(row: dict[str, str]) -> OrderPricePolicy:
|
|
471
|
+
price_policy = row["price_policy"]
|
|
472
|
+
condition_price_policy = row.get("condition_price_policy", "")
|
|
473
|
+
for field_name, policy in (
|
|
474
|
+
("price_policy", price_policy),
|
|
475
|
+
("condition_price_policy", condition_price_policy),
|
|
476
|
+
):
|
|
477
|
+
if policy and policy not in ALLOWED_ORDER_PRICE_POLICIES:
|
|
478
|
+
raise CliInternalError(
|
|
479
|
+
f"order_price_policies.csv {field_name} 값은 다음 중 하나여야 합니다: "
|
|
480
|
+
f"{sorted(ALLOWED_ORDER_PRICE_POLICIES)}: {policy!r}"
|
|
481
|
+
)
|
|
482
|
+
return OrderPricePolicy(
|
|
483
|
+
command_path=row["command_path"],
|
|
484
|
+
order_type=row["order_type"],
|
|
485
|
+
price_option=row["price_option"],
|
|
486
|
+
price_policy=price_policy,
|
|
487
|
+
condition_price_option=row.get("condition_price_option", ""),
|
|
488
|
+
condition_price_policy=condition_price_policy,
|
|
489
|
+
description=row.get("description", ""),
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def validate_order_price_policy(
|
|
494
|
+
command_path: str,
|
|
495
|
+
args: argparse.Namespace,
|
|
496
|
+
definitions: list[ArgumentDefinition] | None = None,
|
|
497
|
+
) -> None:
|
|
498
|
+
order_type = getattr(args, "order_type", None)
|
|
499
|
+
if order_type in (None, ""):
|
|
500
|
+
return
|
|
501
|
+
policy = get_order_price_policy(command_path, str(order_type))
|
|
502
|
+
if policy is None:
|
|
503
|
+
return
|
|
504
|
+
argument_definitions = (
|
|
505
|
+
definitions
|
|
506
|
+
if definitions is not None
|
|
507
|
+
else get_argument_definitions(command_path)
|
|
508
|
+
)
|
|
509
|
+
definitions_by_option = {
|
|
510
|
+
definition.option: definition for definition in argument_definitions
|
|
511
|
+
}
|
|
512
|
+
_validate_policy_option(
|
|
513
|
+
command_path=command_path,
|
|
514
|
+
order_type=str(order_type),
|
|
515
|
+
option=policy.price_option,
|
|
516
|
+
policy=policy.price_policy,
|
|
517
|
+
description=policy.description,
|
|
518
|
+
definitions_by_option=definitions_by_option,
|
|
519
|
+
args=args,
|
|
520
|
+
)
|
|
521
|
+
if policy.condition_price_option or policy.condition_price_policy:
|
|
522
|
+
_validate_policy_option(
|
|
523
|
+
command_path=command_path,
|
|
524
|
+
order_type=str(order_type),
|
|
525
|
+
option=policy.condition_price_option,
|
|
526
|
+
policy=policy.condition_price_policy,
|
|
527
|
+
description=policy.description,
|
|
528
|
+
definitions_by_option=definitions_by_option,
|
|
529
|
+
args=args,
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def _validate_policy_option(
|
|
534
|
+
*,
|
|
535
|
+
command_path: str,
|
|
536
|
+
order_type: str,
|
|
537
|
+
option: str,
|
|
538
|
+
policy: str,
|
|
539
|
+
description: str,
|
|
540
|
+
definitions_by_option: dict[str, ArgumentDefinition],
|
|
541
|
+
args: argparse.Namespace,
|
|
542
|
+
) -> None:
|
|
543
|
+
if policy == "optional" or not policy:
|
|
544
|
+
return
|
|
545
|
+
definition = definitions_by_option.get(option)
|
|
546
|
+
if definition is None:
|
|
547
|
+
raise CliInternalError(
|
|
548
|
+
f"{command_path}: 주문 가격 정책이 알 수 없는 옵션을 참조합니다: {option}"
|
|
549
|
+
)
|
|
550
|
+
value = getattr(args, definition.dest, None)
|
|
551
|
+
has_value = value not in (None, "")
|
|
552
|
+
if policy == "required" and not has_value:
|
|
553
|
+
raise CliInputError(
|
|
554
|
+
f"{command_path}: {description or f'{option} 값이 필요합니다.'}\n"
|
|
555
|
+
f"hint: --order-type {order_type}에는 {option} 옵션이 필요합니다."
|
|
556
|
+
)
|
|
557
|
+
if policy == "forbidden" and has_value:
|
|
558
|
+
raise CliInputError(
|
|
559
|
+
f"{command_path}: {description or f'{option} 값을 넣을 수 없습니다.'}\n"
|
|
560
|
+
f"hint: --order-type {order_type}에는 {option} 옵션을 넣지 마세요."
|
|
561
|
+
)
|