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/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ __all__ = ["build_parser", "main"]
2
+
3
+
4
+ def __getattr__(name: str):
5
+ if name in __all__:
6
+ from kiwoom_cli.main import build_parser, main
7
+
8
+ return {"build_parser": build_parser, "main": main}[name]
9
+ raise AttributeError(name)
kiwoom_cli/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ from kiwoom_cli.main import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ main()
@@ -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
+ )