kiwoom-cli 0.7.0__tar.gz → 1.0.1__tar.gz

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 (34) hide show
  1. {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/PKG-INFO +3 -4
  2. {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/README.md +0 -2
  3. {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli/__init__.py +1 -1
  4. {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli/commands/order.py +76 -37
  5. {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli/formatters.py +4 -3
  6. {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli/main.py +12 -10
  7. {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli/secure_store.py +9 -25
  8. {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli.egg-info/PKG-INFO +3 -4
  9. {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli.egg-info/SOURCES.txt +2 -1
  10. {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli.egg-info/requires.txt +1 -0
  11. {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/pyproject.toml +6 -2
  12. {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/tests/test_cli.py +38 -1
  13. kiwoom_cli-1.0.1/tests/test_secure_store.py +54 -0
  14. {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/LICENSE +0 -0
  15. {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli/api_spec.py +0 -0
  16. {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli/auth.py +0 -0
  17. {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli/client.py +0 -0
  18. {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli/commands/__init__.py +0 -0
  19. {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli/commands/_constants.py +0 -0
  20. {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli/commands/account.py +0 -0
  21. {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli/commands/dashboard.py +0 -0
  22. {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli/commands/market.py +0 -0
  23. {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli/commands/stock.py +0 -0
  24. {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli/commands/stream.py +0 -0
  25. {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli/commands/watch.py +0 -0
  26. {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli/config.py +0 -0
  27. {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli/output.py +0 -0
  28. {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli/streaming.py +0 -0
  29. {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli.egg-info/dependency_links.txt +0 -0
  30. {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli.egg-info/entry_points.txt +0 -0
  31. {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli.egg-info/top_level.txt +0 -0
  32. {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/setup.cfg +0 -0
  33. {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/tests/test_client.py +0 -0
  34. {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/tests/test_formatters.py +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kiwoom-cli
3
- Version: 0.7.0
3
+ Version: 1.0.1
4
4
  Summary: User-friendly CLI for the Kiwoom Securities REST API (키움증권)
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/gejyn14/kiwoom-cli
7
7
  Project-URL: Repository, https://github.com/gejyn14/kiwoom-cli
8
8
  Project-URL: Issues, https://github.com/gejyn14/kiwoom-cli/issues
9
9
  Keywords: kiwoom,stock,trading,cli,korea,finance
10
- Classifier: Development Status :: 4 - Beta
10
+ Classifier: Development Status :: 5 - Production/Stable
11
11
  Classifier: Environment :: Console
12
12
  Classifier: Intended Audience :: Financial and Insurance Industry
13
13
  Classifier: License :: OSI Approved :: MIT License
@@ -26,6 +26,7 @@ Requires-Dist: tomli>=2.0; python_version < "3.11"
26
26
  Requires-Dist: tomli-w>=1.0
27
27
  Requires-Dist: keyring>=25.0
28
28
  Requires-Dist: websockets>=12.0
29
+ Requires-Dist: cryptography>=42.0
29
30
  Provides-Extra: dev
30
31
  Requires-Dist: pytest>=8.0; extra == "dev"
31
32
  Requires-Dist: pytest-httpx>=0.30; extra == "dev"
@@ -444,8 +445,6 @@ eval (env _KIWOOM_COMPLETE=fish_source kiwoom)
444
445
 
445
446
  | 항목 | 값 |
446
447
  | ---------------- | ---------------------------------------- |
447
- | 항목 | 값 |
448
- | --------------- | ---------------------------- |
449
448
  | 설정 파일 | `~/.kiwoom/config.toml` (도메인, 계좌만) |
450
449
  | appkey/secretkey | OS 키체인 (암호화 저장) |
451
450
  | 토큰 | OS 키체인 |
@@ -410,8 +410,6 @@ eval (env _KIWOOM_COMPLETE=fish_source kiwoom)
410
410
 
411
411
  | 항목 | 값 |
412
412
  | ---------------- | ---------------------------------------- |
413
- | 항목 | 값 |
414
- | --------------- | ---------------------------- |
415
413
  | 설정 파일 | `~/.kiwoom/config.toml` (도메인, 계좌만) |
416
414
  | appkey/secretkey | OS 키체인 (암호화 저장) |
417
415
  | 토큰 | OS 키체인 |
@@ -1,3 +1,3 @@
1
1
  """Kiwoom Securities REST API CLI."""
2
2
 
3
- __version__ = "0.7.0"
3
+ __version__ = "1.0.1"
@@ -1,6 +1,7 @@
1
1
  """Order management commands.
2
2
 
3
- All order commands require --confirm flag for safety.
3
+ Order commands prompt for confirmation by default. Use --confirm to skip
4
+ the prompt (for scripts/automation).
4
5
 
5
6
  Subgroups:
6
7
  order buy/sell/modify/cancel - Stock orders (kt10000-kt10003)
@@ -83,12 +84,15 @@ def order():
83
84
  @click.option("--type", "order_type", default="market", type=click.Choice(list(ORDER_TYPES.keys())), help="주문유형")
84
85
  @click.option("--exchange", "stex", default="KRX", type=click.Choice(["KRX", "NXT", "SOR"]), help="거래소")
85
86
  @click.option("--cond-price", "cond_uv", type=int, default=0, help="조건부가격 (스톱지정가 등)")
86
- @click.option("--confirm", is_flag=True, required=True, help="주문 확인 (필수)")
87
+ @click.option("--confirm", is_flag=True, help="확인 프롬프트 없이 주문 실행")
87
88
  def buy(code: str, qty: int, price: int, order_type: str, stex: str, cond_uv: int, confirm: bool):
88
89
  """주식 매수주문 (kt10000).
89
90
 
90
91
  예: kiwoom order buy 005930 10 --price 70000 --type limit --confirm
91
92
  """
93
+ if not confirm:
94
+ click.confirm("주문을 실행하시겠습니까?", abort=True)
95
+
92
96
  _show_order_preview("매수", code, qty, price, order_type, stex)
93
97
 
94
98
  with KiwoomClient() as c:
@@ -110,12 +114,15 @@ def buy(code: str, qty: int, price: int, order_type: str, stex: str, cond_uv: in
110
114
  @click.option("--type", "order_type", default="market", type=click.Choice(list(ORDER_TYPES.keys())), help="주문유형")
111
115
  @click.option("--exchange", "stex", default="KRX", type=click.Choice(["KRX", "NXT", "SOR"]), help="거래소")
112
116
  @click.option("--cond-price", "cond_uv", type=int, default=0, help="조건부가격 (스톱지정가 등)")
113
- @click.option("--confirm", is_flag=True, required=True, help="주문 확인 (필수)")
117
+ @click.option("--confirm", is_flag=True, help="확인 프롬프트 없이 주문 실행")
114
118
  def sell(code: str, qty: int, price: int, order_type: str, stex: str, cond_uv: int, confirm: bool):
115
119
  """주식 매도주문 (kt10001).
116
120
 
117
121
  예: kiwoom order sell 005930 10 --type market --confirm
118
122
  """
123
+ if not confirm:
124
+ click.confirm("주문을 실행하시겠습니까?", abort=True)
125
+
119
126
  _show_order_preview("매도", code, qty, price, order_type, stex)
120
127
 
121
128
  with KiwoomClient() as c:
@@ -137,12 +144,15 @@ def sell(code: str, qty: int, price: int, order_type: str, stex: str, cond_uv: i
137
144
  @click.argument("price", type=int)
138
145
  @click.option("--exchange", "stex", default="KRX", type=click.Choice(["KRX", "NXT", "SOR"]), help="거래소")
139
146
  @click.option("--cond-price", "mdfy_cond_uv", type=int, default=0, help="정정 조건부가격")
140
- @click.option("--confirm", is_flag=True, required=True, help="주문 확인 (필수)")
147
+ @click.option("--confirm", is_flag=True, help="확인 프롬프트 없이 주문 실행")
141
148
  def modify(orig_order_no: str, code: str, qty: int, price: int, stex: str, mdfy_cond_uv: int, confirm: bool):
142
149
  """주식 정정주문 (kt10002).
143
150
 
144
151
  예: kiwoom order modify 0000139 005930 1 70000 --confirm
145
152
  """
153
+ if not confirm:
154
+ click.confirm("주문을 실행하시겠습니까?", abort=True)
155
+
146
156
  console.print(f"[yellow]정정 주문:[/] 원주문번호={orig_order_no} 종목={code} 수량={qty} 가격={price:,}")
147
157
 
148
158
  with KiwoomClient() as c:
@@ -162,12 +172,15 @@ def modify(orig_order_no: str, code: str, qty: int, price: int, stex: str, mdfy_
162
172
  @click.argument("code")
163
173
  @click.option("--qty", type=int, default=0, help="취소수량 (0=전량취소)")
164
174
  @click.option("--exchange", "stex", default="KRX", type=click.Choice(["KRX", "NXT", "SOR"]), help="거래소")
165
- @click.option("--confirm", is_flag=True, required=True, help="주문 확인 (필수)")
175
+ @click.option("--confirm", is_flag=True, help="확인 프롬프트 없이 주문 실행")
166
176
  def cancel(orig_order_no: str, code: str, qty: int, stex: str, confirm: bool):
167
177
  """주식 취소주문 (kt10003).
168
178
 
169
179
  예: kiwoom order cancel 0000140 005930 --confirm
170
180
  """
181
+ if not confirm:
182
+ click.confirm("주문을 실행하시겠습니까?", abort=True)
183
+
171
184
  console.print(f"[yellow]취소 주문:[/] 원주문번호={orig_order_no} 종목={code} 수량={qty or '전량'}")
172
185
 
173
186
  with KiwoomClient() as c:
@@ -197,12 +210,15 @@ def credit():
197
210
  @click.option("--type", "order_type", default="market", type=click.Choice(list(ORDER_TYPES.keys())), help="주문유형")
198
211
  @click.option("--exchange", "stex", default="KRX", type=click.Choice(["KRX", "NXT", "SOR"]), help="거래소")
199
212
  @click.option("--cond-price", "cond_uv", type=int, default=0, help="조건부가격")
200
- @click.option("--confirm", is_flag=True, required=True, help="주문 확인 (필수)")
213
+ @click.option("--confirm", is_flag=True, help="확인 프롬프트 없이 주문 실행")
201
214
  def credit_buy(code: str, qty: int, price: int, order_type: str, stex: str, cond_uv: int, confirm: bool):
202
215
  """신용 매수주문 (kt10006).
203
216
 
204
217
  예: kiwoom order credit buy 005930 10 --type limit --price 70000 --confirm
205
218
  """
219
+ if not confirm:
220
+ click.confirm("주문을 실행하시겠습니까?", abort=True)
221
+
206
222
  _show_order_preview("신용 매수", code, qty, price, order_type, stex)
207
223
 
208
224
  with KiwoomClient() as c:
@@ -224,12 +240,15 @@ def credit_buy(code: str, qty: int, price: int, order_type: str, stex: str, cond
224
240
  @click.option("--type", "order_type", default="market", type=click.Choice(list(ORDER_TYPES.keys())), help="주문유형")
225
241
  @click.option("--exchange", "stex", default="KRX", type=click.Choice(["KRX", "NXT", "SOR"]), help="거래소")
226
242
  @click.option("--cond-price", "cond_uv", type=int, default=0, help="조건부가격")
227
- @click.option("--confirm", is_flag=True, required=True, help="주문 확인 (필수)")
243
+ @click.option("--confirm", is_flag=True, help="확인 프롬프트 없이 주문 실행")
228
244
  def credit_sell(code: str, qty: int, price: int, order_type: str, stex: str, cond_uv: int, confirm: bool):
229
245
  """신용 매도주문 (kt10007).
230
246
 
231
247
  예: kiwoom order credit sell 005930 10 --type market --confirm
232
248
  """
249
+ if not confirm:
250
+ click.confirm("주문을 실행하시겠습니까?", abort=True)
251
+
233
252
  _show_order_preview("신용 매도", code, qty, price, order_type, stex)
234
253
 
235
254
  with KiwoomClient() as c:
@@ -251,12 +270,15 @@ def credit_sell(code: str, qty: int, price: int, order_type: str, stex: str, con
251
270
  @click.argument("price", type=int)
252
271
  @click.option("--exchange", "stex", default="KRX", type=click.Choice(["KRX", "NXT", "SOR"]), help="거래소")
253
272
  @click.option("--cond-price", "mdfy_cond_uv", type=int, default=0, help="정정 조건부가격")
254
- @click.option("--confirm", is_flag=True, required=True, help="주문 확인 (필수)")
273
+ @click.option("--confirm", is_flag=True, help="확인 프롬프트 없이 주문 실행")
255
274
  def credit_modify(orig_order_no: str, code: str, qty: int, price: int, stex: str, mdfy_cond_uv: int, confirm: bool):
256
275
  """신용 정정주문 (kt10008).
257
276
 
258
277
  예: kiwoom order credit modify 0000139 005930 1 70000 --confirm
259
278
  """
279
+ if not confirm:
280
+ click.confirm("주문을 실행하시겠습니까?", abort=True)
281
+
260
282
  console.print(f"[yellow]신용 정정 주문:[/] 원주문번호={orig_order_no} 종목={code} 수량={qty} 가격={price:,}")
261
283
 
262
284
  with KiwoomClient() as c:
@@ -276,12 +298,15 @@ def credit_modify(orig_order_no: str, code: str, qty: int, price: int, stex: str
276
298
  @click.argument("code")
277
299
  @click.option("--qty", type=int, default=0, help="취소수량 (0=전량취소)")
278
300
  @click.option("--exchange", "stex", default="KRX", type=click.Choice(["KRX", "NXT", "SOR"]), help="거래소")
279
- @click.option("--confirm", is_flag=True, required=True, help="주문 확인 (필수)")
301
+ @click.option("--confirm", is_flag=True, help="확인 프롬프트 없이 주문 실행")
280
302
  def credit_cancel(orig_order_no: str, code: str, qty: int, stex: str, confirm: bool):
281
303
  """신용 취소주문 (kt10009).
282
304
 
283
305
  예: kiwoom order credit cancel 0000140 005930 --confirm
284
306
  """
307
+ if not confirm:
308
+ click.confirm("주문을 실행하시겠습니까?", abort=True)
309
+
285
310
  console.print(f"[yellow]신용 취소 주문:[/] 원주문번호={orig_order_no} 종목={code} 수량={qty or '전량'}")
286
311
 
287
312
  with KiwoomClient() as c:
@@ -310,12 +335,15 @@ def gold():
310
335
  @click.option("--price", type=int, default=0, help="주문가격 (시장가 주문시 생략)")
311
336
  @click.option("--type", "order_type", default="market", type=click.Choice(list(ORDER_TYPES.keys())), help="주문유형")
312
337
  @click.option("--exchange", "stex", default="KRX", type=click.Choice(["KRX", "NXT", "SOR"]), help="거래소")
313
- @click.option("--confirm", is_flag=True, required=True, help="주문 확인 (필수)")
338
+ @click.option("--confirm", is_flag=True, help="확인 프롬프트 없이 주문 실행")
314
339
  def gold_buy(code: str, qty: int, price: int, order_type: str, stex: str, confirm: bool):
315
340
  """금현물 매수주문 (kt50000).
316
341
 
317
342
  예: kiwoom order gold buy 730060 10 --type limit --price 90000 --confirm
318
343
  """
344
+ if not confirm:
345
+ click.confirm("주문을 실행하시겠습니까?", abort=True)
346
+
319
347
  _show_order_preview("금현물 매수", code, qty, price, order_type, stex)
320
348
 
321
349
  with KiwoomClient() as c:
@@ -335,12 +363,15 @@ def gold_buy(code: str, qty: int, price: int, order_type: str, stex: str, confir
335
363
  @click.option("--price", type=int, default=0, help="주문가격 (시장가 주문시 생략)")
336
364
  @click.option("--type", "order_type", default="market", type=click.Choice(list(ORDER_TYPES.keys())), help="주문유형")
337
365
  @click.option("--exchange", "stex", default="KRX", type=click.Choice(["KRX", "NXT", "SOR"]), help="거래소")
338
- @click.option("--confirm", is_flag=True, required=True, help="주문 확인 (필수)")
366
+ @click.option("--confirm", is_flag=True, help="확인 프롬프트 없이 주문 실행")
339
367
  def gold_sell(code: str, qty: int, price: int, order_type: str, stex: str, confirm: bool):
340
368
  """금현물 매도주문 (kt50001).
341
369
 
342
370
  예: kiwoom order gold sell 730060 10 --type market --confirm
343
371
  """
372
+ if not confirm:
373
+ click.confirm("주문을 실행하시겠습니까?", abort=True)
374
+
344
375
  _show_order_preview("금현물 매도", code, qty, price, order_type, stex)
345
376
 
346
377
  with KiwoomClient() as c:
@@ -360,12 +391,15 @@ def gold_sell(code: str, qty: int, price: int, order_type: str, stex: str, confi
360
391
  @click.argument("qty", type=int)
361
392
  @click.argument("price", type=int)
362
393
  @click.option("--exchange", "stex", default="KRX", type=click.Choice(["KRX", "NXT", "SOR"]), help="거래소")
363
- @click.option("--confirm", is_flag=True, required=True, help="주문 확인 (필수)")
394
+ @click.option("--confirm", is_flag=True, help="확인 프롬프트 없이 주문 실행")
364
395
  def gold_modify(orig_order_no: str, code: str, qty: int, price: int, stex: str, confirm: bool):
365
396
  """금현물 정정주문 (kt50002).
366
397
 
367
398
  예: kiwoom order gold modify 0000139 730060 1 90000 --confirm
368
399
  """
400
+ if not confirm:
401
+ click.confirm("주문을 실행하시겠습니까?", abort=True)
402
+
369
403
  console.print(f"[yellow]금현물 정정 주문:[/] 원주문번호={orig_order_no} 종목={code} 수량={qty} 가격={price:,}")
370
404
 
371
405
  with KiwoomClient() as c:
@@ -384,12 +418,15 @@ def gold_modify(orig_order_no: str, code: str, qty: int, price: int, stex: str,
384
418
  @click.argument("code")
385
419
  @click.option("--qty", type=int, default=0, help="취소수량 (0=전량취소)")
386
420
  @click.option("--exchange", "stex", default="KRX", type=click.Choice(["KRX", "NXT", "SOR"]), help="거래소")
387
- @click.option("--confirm", is_flag=True, required=True, help="주문 확인 (필수)")
421
+ @click.option("--confirm", is_flag=True, help="확인 프롬프트 없이 주문 실행")
388
422
  def gold_cancel(orig_order_no: str, code: str, qty: int, stex: str, confirm: bool):
389
423
  """금현물 취소주문 (kt50003).
390
424
 
391
425
  예: kiwoom order gold cancel 0000140 730060 --confirm
392
426
  """
427
+ if not confirm:
428
+ click.confirm("주문을 실행하시겠습니까?", abort=True)
429
+
393
430
  console.print(f"[yellow]금현물 취소 주문:[/] 원주문번호={orig_order_no} 종목={code} 수량={qty or '전량'}")
394
431
 
395
432
  with KiwoomClient() as c:
@@ -405,11 +442,10 @@ def gold_cancel(orig_order_no: str, code: str, qty: int, stex: str, confirm: boo
405
442
  # ── Gold Account Queries ───────────────────────────────
406
443
 
407
444
  @gold.command("balance")
408
- @click.option("--confirm", is_flag=True, required=True, help="조회 확인 (필수)")
409
- def gold_balance(confirm: bool):
445
+ def gold_balance():
410
446
  """금현물 잔고확인 (kt50020).
411
447
 
412
- 예: kiwoom order gold balance --confirm
448
+ 예: kiwoom order gold balance
413
449
  """
414
450
  with KiwoomClient() as c:
415
451
  data, _ = c.request("kt50020", {})
@@ -417,11 +453,10 @@ def gold_balance(confirm: bool):
417
453
 
418
454
 
419
455
  @gold.command("deposit")
420
- @click.option("--confirm", is_flag=True, required=True, help="조회 확인 (필수)")
421
- def gold_deposit(confirm: bool):
456
+ def gold_deposit():
422
457
  """금현물 예수금 (kt50021).
423
458
 
424
- 예: kiwoom order gold deposit --confirm
459
+ 예: kiwoom order gold deposit
425
460
  """
426
461
  with KiwoomClient() as c:
427
462
  data, _ = c.request("kt50021", {})
@@ -429,11 +464,10 @@ def gold_deposit(confirm: bool):
429
464
 
430
465
 
431
466
  @gold.command("executions-all")
432
- @click.option("--confirm", is_flag=True, required=True, help="조회 확인 (필수)")
433
- def gold_executions_all(confirm: bool):
467
+ def gold_executions_all():
434
468
  """금현물 주문체결전체조회 (kt50030).
435
469
 
436
- 예: kiwoom order gold executions-all --confirm
470
+ 예: kiwoom order gold executions-all
437
471
  """
438
472
  with KiwoomClient() as c:
439
473
  data, _ = c.request("kt50030", {})
@@ -441,11 +475,10 @@ def gold_executions_all(confirm: bool):
441
475
 
442
476
 
443
477
  @gold.command("executions")
444
- @click.option("--confirm", is_flag=True, required=True, help="조회 확인 (필수)")
445
- def gold_executions(confirm: bool):
478
+ def gold_executions():
446
479
  """금현물 주문체결조회 (kt50031).
447
480
 
448
- 예: kiwoom order gold executions --confirm
481
+ 예: kiwoom order gold executions
449
482
  """
450
483
  with KiwoomClient() as c:
451
484
  data, _ = c.request("kt50031", {})
@@ -453,11 +486,10 @@ def gold_executions(confirm: bool):
453
486
 
454
487
 
455
488
  @gold.command("history")
456
- @click.option("--confirm", is_flag=True, required=True, help="조회 확인 (필수)")
457
- def gold_history(confirm: bool):
489
+ def gold_history():
458
490
  """금현물 거래내역조회 (kt50032).
459
491
 
460
- 예: kiwoom order gold history --confirm
492
+ 예: kiwoom order gold history
461
493
  """
462
494
  with KiwoomClient() as c:
463
495
  data, _ = c.request("kt50032", {})
@@ -465,11 +497,10 @@ def gold_history(confirm: bool):
465
497
 
466
498
 
467
499
  @gold.command("pending")
468
- @click.option("--confirm", is_flag=True, required=True, help="조회 확인 (필수)")
469
- def gold_pending(confirm: bool):
500
+ def gold_pending():
470
501
  """금현물 미체결조회 (kt50075).
471
502
 
472
- 예: kiwoom order gold pending --confirm
503
+ 예: kiwoom order gold pending
473
504
  """
474
505
  with KiwoomClient() as c:
475
506
  data, _ = c.request("kt50075", {})
@@ -487,11 +518,10 @@ def condition():
487
518
 
488
519
 
489
520
  @condition.command("list")
490
- @click.option("--confirm", is_flag=True, required=True, help="조회 확인 (필수)")
491
- def condition_list(confirm: bool):
521
+ def condition_list():
492
522
  """조건검색 목록조회 (ka10171).
493
523
 
494
- 예: kiwoom order condition list --confirm
524
+ 예: kiwoom order condition list
495
525
  """
496
526
  with KiwoomClient() as c:
497
527
  data, _ = c.request("ka10171", {
@@ -505,12 +535,15 @@ def condition_list(confirm: bool):
505
535
  @click.option("--exchange", "stex_tp", default="K", type=click.Choice(["K"]), help="거래소 (K=KRX)")
506
536
  @click.option("--cont-yn", default="", help="연속조회여부")
507
537
  @click.option("--next-key", default="", help="연속조회키")
508
- @click.option("--confirm", is_flag=True, required=True, help="조회 확인 (필수)")
538
+ @click.option("--confirm", is_flag=True, help="확인 프롬프트 없이 주문 실행")
509
539
  def condition_search(seq: str, stex_tp: str, cont_yn: str, next_key: str, confirm: bool):
510
540
  """조건검색 요청 일반 (ka10172).
511
541
 
512
542
  예: kiwoom order condition search 001 --confirm
513
543
  """
544
+ if not confirm:
545
+ click.confirm("주문을 실행하시겠습니까?", abort=True)
546
+
514
547
  body = {
515
548
  "trnm": "CNSRREQ",
516
549
  "seq": seq,
@@ -530,12 +563,15 @@ def condition_search(seq: str, stex_tp: str, cont_yn: str, next_key: str, confir
530
563
  @condition.command("realtime")
531
564
  @click.argument("seq")
532
565
  @click.option("--exchange", "stex_tp", default="K", type=click.Choice(["K"]), help="거래소 (K=KRX)")
533
- @click.option("--confirm", is_flag=True, required=True, help="조회 확인 (필수)")
566
+ @click.option("--confirm", is_flag=True, help="확인 프롬프트 없이 주문 실행")
534
567
  def condition_realtime(seq: str, stex_tp: str, confirm: bool):
535
568
  """조건검색 요청 실시간 (ka10173).
536
569
 
537
570
  예: kiwoom order condition realtime 001 --confirm
538
571
  """
572
+ if not confirm:
573
+ click.confirm("주문을 실행하시겠습니까?", abort=True)
574
+
539
575
  with KiwoomClient() as c:
540
576
  data, _ = c.request("ka10173", {
541
577
  "trnm": "CNSRREQ",
@@ -548,12 +584,15 @@ def condition_realtime(seq: str, stex_tp: str, confirm: bool):
548
584
 
549
585
  @condition.command("stop")
550
586
  @click.argument("seq")
551
- @click.option("--confirm", is_flag=True, required=True, help="조회 확인 (필수)")
587
+ @click.option("--confirm", is_flag=True, help="확인 프롬프트 없이 주문 실행")
552
588
  def condition_stop(seq: str, confirm: bool):
553
589
  """조건검색 실시간 해제 (ka10174).
554
590
 
555
591
  예: kiwoom order condition stop 001 --confirm
556
592
  """
593
+ if not confirm:
594
+ click.confirm("주문을 실행하시겠습니까?", abort=True)
595
+
557
596
  with KiwoomClient() as c:
558
597
  data, _ = c.request("ka10174", {
559
598
  "trnm": "CNSRCLR",
@@ -51,10 +51,11 @@ def _flat_dict(data: dict) -> list[dict]:
51
51
 
52
52
  def _sign_color(value: str) -> str:
53
53
  """Return color based on sign: red for positive (상승), blue for negative (하락)."""
54
- v = value.strip().lstrip("+").lstrip("-")
55
- if value.strip().startswith("-"):
54
+ s = value.strip()
55
+ if s.startswith("-"):
56
56
  return "blue"
57
- if value.strip().startswith("+") or (v and v != "0" and not value.strip().startswith("-")):
57
+ v = s.lstrip("+").lstrip("0")
58
+ if v and (v[:1].isdigit() or v[:1] == "."):
58
59
  return "red"
59
60
  return "white"
60
61
 
@@ -108,17 +108,20 @@ def config_cmd():
108
108
  @click.option("--profile", default="default", help="프로필 이름")
109
109
  @click.option("--appkey", prompt="App Key", help="키움 API App Key")
110
110
  @click.option("--secretkey", prompt="Secret Key", hide_input=True, help="키움 API Secret Key")
111
- @click.option("--domain", type=click.Choice(["prod", "mock"]), default="mock", help="도메인 (prod: 실거래, mock: 모의투자)")
112
- @click.option("--account", default="", help="기본 계좌번호")
111
+ @click.option("--domain", prompt="도메인 (prod=실거래, mock=모의투자)", type=click.Choice(["prod", "mock"]), default="mock", help="도메인")
112
+ @click.option("--account", prompt="계좌번호 (없으면 Enter)", default="", help="계좌번호")
113
113
  def config_setup(profile: str, appkey: str, secretkey: str, domain: str, account: str):
114
114
  """초기 설정 (App Key, Secret Key, 도메인)."""
115
115
  password = click.prompt("시스템 비밀번호 (인증정보 암호화에 사용)", hide_input=True)
116
- if not config.store.is_initialized:
117
- config.store.setup(password)
118
- else:
116
+ if config.store.is_initialized:
119
117
  if not config.store.unlock(password):
120
- console.print("[red]비밀번호가 일치하지 않습니다.[/]")
121
- raise SystemExit(1)
118
+ # Unlock failed — possibly old encryption format. Offer re-init.
119
+ if click.confirm("비밀번호 검증 실패. 암호화 저장소를 재설정하시겠습니까?"):
120
+ config.store.setup(password)
121
+ else:
122
+ raise SystemExit(1)
123
+ else:
124
+ config.store.setup(password)
122
125
  config.set_appkey(appkey, profile=profile)
123
126
  config.set_secretkey(secretkey, profile=profile)
124
127
  cfg = config.load_config()
@@ -202,9 +205,8 @@ def config_profiles():
202
205
  for name, settings in profiles.items():
203
206
  marker = " [green]*[/]" if name == default else " "
204
207
  domain = settings.get("domain", "mock")
205
- account = settings.get("account", "")
206
- account_str = f" 계좌={account}" if account else ""
207
- console.print(f" {marker} {name:15s} 도메인={domain}{account_str}")
208
+ account = settings.get("account", "") or "(미설정)"
209
+ console.print(f" {marker} {name:15s} 도메인={domain} 계좌={account}")
208
210
 
209
211
 
210
212
  # ── Auth ──────────────────────────────────────────────
@@ -17,11 +17,10 @@ from __future__ import annotations
17
17
 
18
18
  import base64
19
19
  import hashlib
20
- import json
21
- import os
22
20
  import secrets
23
21
 
24
22
  import keyring
23
+ from cryptography.fernet import Fernet, InvalidToken
25
24
 
26
25
 
27
26
  class SecureStoreError(Exception):
@@ -35,9 +34,9 @@ class SecureStoreLocked(SecureStoreError):
35
34
  class SecureStore:
36
35
  """Encrypted credential store backed by OS keychain.
37
36
 
38
- Values are encrypted with AES-like XOR cipher keyed by a password-derived
39
- key. The encryption key never touches disk — it exists only in memory
40
- after unlock.
37
+ Values are encrypted with Fernet (AES-128-CBC + HMAC) keyed by a
38
+ password-derived key. The encryption key never touches disk — it exists
39
+ only in memory after unlock.
41
40
  """
42
41
 
43
42
  def __init__(self, service: str):
@@ -78,7 +77,7 @@ class SecureStore:
78
77
  self._key = None
79
78
  return False
80
79
  return True
81
- except Exception:
80
+ except (InvalidToken, Exception):
82
81
  self._key = None
83
82
  return False
84
83
 
@@ -114,36 +113,21 @@ class SecureStore:
114
113
 
115
114
  def _derive_key(self, password: str, salt: str) -> bytes:
116
115
  """Derive encryption key from password + salt using PBKDF2."""
117
- return hashlib.pbkdf2_hmac(
116
+ raw = hashlib.pbkdf2_hmac(
118
117
  "sha256",
119
118
  password.encode("utf-8"),
120
119
  salt.encode("utf-8"),
121
120
  iterations=100_000,
122
121
  dklen=32,
123
122
  )
123
+ return base64.urlsafe_b64encode(raw)
124
124
 
125
125
  def _encrypt(self, data: bytes) -> str:
126
126
  """Encrypt data with the derived key."""
127
127
  assert self._key is not None
128
- iv = os.urandom(16)
129
- encrypted = bytes(a ^ b for a, b in zip(data, self._expand_key(len(data), iv)))
130
- payload = {"iv": base64.b64encode(iv).decode(), "data": base64.b64encode(encrypted).decode()}
131
- return base64.b64encode(json.dumps(payload).encode()).decode()
128
+ return Fernet(self._key).encrypt(data).decode("ascii")
132
129
 
133
130
  def _decrypt(self, token: str) -> bytes:
134
131
  """Decrypt data with the derived key."""
135
132
  assert self._key is not None
136
- payload = json.loads(base64.b64decode(token))
137
- iv = base64.b64decode(payload["iv"])
138
- encrypted = base64.b64decode(payload["data"])
139
- return bytes(a ^ b for a, b in zip(encrypted, self._expand_key(len(encrypted), iv)))
140
-
141
- def _expand_key(self, length: int, iv: bytes) -> bytes:
142
- """Expand key to match data length using HMAC-based stream."""
143
- result = b""
144
- counter = 0
145
- while len(result) < length:
146
- block = hashlib.sha256(self._key + iv + counter.to_bytes(4, "big")).digest()
147
- result += block
148
- counter += 1
149
- return result[:length]
133
+ return Fernet(self._key).decrypt(token.encode("ascii"))
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kiwoom-cli
3
- Version: 0.7.0
3
+ Version: 1.0.1
4
4
  Summary: User-friendly CLI for the Kiwoom Securities REST API (키움증권)
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/gejyn14/kiwoom-cli
7
7
  Project-URL: Repository, https://github.com/gejyn14/kiwoom-cli
8
8
  Project-URL: Issues, https://github.com/gejyn14/kiwoom-cli/issues
9
9
  Keywords: kiwoom,stock,trading,cli,korea,finance
10
- Classifier: Development Status :: 4 - Beta
10
+ Classifier: Development Status :: 5 - Production/Stable
11
11
  Classifier: Environment :: Console
12
12
  Classifier: Intended Audience :: Financial and Insurance Industry
13
13
  Classifier: License :: OSI Approved :: MIT License
@@ -26,6 +26,7 @@ Requires-Dist: tomli>=2.0; python_version < "3.11"
26
26
  Requires-Dist: tomli-w>=1.0
27
27
  Requires-Dist: keyring>=25.0
28
28
  Requires-Dist: websockets>=12.0
29
+ Requires-Dist: cryptography>=42.0
29
30
  Provides-Extra: dev
30
31
  Requires-Dist: pytest>=8.0; extra == "dev"
31
32
  Requires-Dist: pytest-httpx>=0.30; extra == "dev"
@@ -444,8 +445,6 @@ eval (env _KIWOOM_COMPLETE=fish_source kiwoom)
444
445
 
445
446
  | 항목 | 값 |
446
447
  | ---------------- | ---------------------------------------- |
447
- | 항목 | 값 |
448
- | --------------- | ---------------------------- |
449
448
  | 설정 파일 | `~/.kiwoom/config.toml` (도메인, 계좌만) |
450
449
  | appkey/secretkey | OS 키체인 (암호화 저장) |
451
450
  | 토큰 | OS 키체인 |
@@ -28,4 +28,5 @@ kiwoom_cli/commands/stream.py
28
28
  kiwoom_cli/commands/watch.py
29
29
  tests/test_cli.py
30
30
  tests/test_client.py
31
- tests/test_formatters.py
31
+ tests/test_formatters.py
32
+ tests/test_secure_store.py
@@ -4,6 +4,7 @@ rich>=13.0
4
4
  tomli-w>=1.0
5
5
  keyring>=25.0
6
6
  websockets>=12.0
7
+ cryptography>=42.0
7
8
 
8
9
  [:python_version < "3.11"]
9
10
  tomli>=2.0
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "kiwoom-cli"
7
- version = "0.7.0"
7
+ dynamic = ["version"]
8
8
  description = "User-friendly CLI for the Kiwoom Securities REST API (키움증권)"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
11
11
  requires-python = ">=3.10"
12
12
  keywords = ["kiwoom", "stock", "trading", "cli", "korea", "finance"]
13
13
  classifiers = [
14
- "Development Status :: 4 - Beta",
14
+ "Development Status :: 5 - Production/Stable",
15
15
  "Environment :: Console",
16
16
  "Intended Audience :: Financial and Insurance Industry",
17
17
  "License :: OSI Approved :: MIT License",
@@ -29,6 +29,7 @@ dependencies = [
29
29
  "tomli-w>=1.0",
30
30
  "keyring>=25.0",
31
31
  "websockets>=12.0",
32
+ "cryptography>=42.0",
32
33
  ]
33
34
 
34
35
  [project.optional-dependencies]
@@ -44,3 +45,6 @@ kiwoom = "kiwoom_cli.main:cli"
44
45
 
45
46
  [tool.setuptools.packages.find]
46
47
  include = ["kiwoom_cli*"]
48
+
49
+ [tool.setuptools.dynamic]
50
+ version = {attr = "kiwoom_cli.__version__"}
@@ -6,6 +6,7 @@ from unittest.mock import patch, MagicMock
6
6
  import pytest
7
7
  from click.testing import CliRunner
8
8
 
9
+ from kiwoom_cli import __version__
9
10
  from kiwoom_cli.main import cli
10
11
 
11
12
 
@@ -17,7 +18,7 @@ def runner():
17
18
  def test_version(runner):
18
19
  result = runner.invoke(cli, ["--version"])
19
20
  assert result.exit_code == 0
20
- assert "0.7.0" in result.output
21
+ assert __version__ in result.output
21
22
 
22
23
 
23
24
  def test_help(runner):
@@ -141,3 +142,39 @@ def test_api_error_handling(runner):
141
142
  result = runner.invoke(cli, ["stock", "info", "005930"])
142
143
  assert result.exit_code == 2
143
144
  assert "오류" in result.output
145
+
146
+
147
+ # Test order --confirm behavior
148
+ @patch("kiwoom_cli.commands.order.KiwoomClient")
149
+ def test_order_buy_without_confirm_prompts(mock_cls, runner):
150
+ """Order without --confirm should prompt for confirmation."""
151
+ mock_client = MagicMock()
152
+ mock_client.__enter__ = lambda s: s
153
+ mock_client.__exit__ = MagicMock(return_value=False)
154
+ mock_cls.return_value = mock_client
155
+ # Without --confirm, answering 'n' should abort
156
+ result = runner.invoke(cli, ["order", "buy", "005930", "10", "--type", "market"], input="n\n")
157
+ assert result.exit_code != 0 or "Aborted" in result.output
158
+
159
+
160
+ @patch("kiwoom_cli.commands.order.KiwoomClient")
161
+ def test_order_buy_with_confirm_skips_prompt(mock_cls, runner):
162
+ """Order with --confirm should skip prompt."""
163
+ mock_client = MagicMock()
164
+ mock_client.request.return_value = ({"return_code": 0, "ord_no": "001", "return_msg": "OK"}, {})
165
+ mock_client.__enter__ = lambda s: s
166
+ mock_client.__exit__ = MagicMock(return_value=False)
167
+ mock_cls.return_value = mock_client
168
+ result = runner.invoke(cli, ["order", "buy", "005930", "10", "--type", "market", "--confirm"])
169
+ assert result.exit_code == 0
170
+
171
+
172
+ def test_config_profiles_command(runner):
173
+ result = runner.invoke(cli, ["config", "profiles"])
174
+ assert result.exit_code == 0
175
+
176
+
177
+ def test_profile_flag_with_show(runner):
178
+ result = runner.invoke(cli, ["-p", "test", "config", "show"])
179
+ assert result.exit_code == 0
180
+ assert "test" in result.output
@@ -0,0 +1,54 @@
1
+ """Tests for SecureStore encrypted credential storage."""
2
+ from kiwoom_cli.secure_store import SecureStore, SecureStoreLocked
3
+ import pytest
4
+
5
+ @pytest.fixture
6
+ def store(tmp_path, monkeypatch):
7
+ """Create a SecureStore with a temporary keyring backend."""
8
+ import keyring
9
+ # Use a dict-based in-memory backend for testing
10
+ store = SecureStore("test-service")
11
+ _data = {}
12
+ monkeypatch.setattr(keyring, "get_password", lambda svc, key: _data.get(f"{svc}:{key}"))
13
+ monkeypatch.setattr(keyring, "set_password", lambda svc, key, val: _data.__setitem__(f"{svc}:{key}", val))
14
+ monkeypatch.setattr(keyring, "delete_password", lambda svc, key: _data.pop(f"{svc}:{key}", None))
15
+ return store
16
+
17
+ def test_setup_and_unlock(store):
18
+ store.setup("mypassword")
19
+ assert store.is_initialized
20
+ store.lock()
21
+ assert not store.is_unlocked
22
+ assert store.unlock("mypassword")
23
+ assert store.is_unlocked
24
+
25
+ def test_wrong_password(store):
26
+ store.setup("correct")
27
+ store.lock()
28
+ assert not store.unlock("wrong")
29
+
30
+ def test_set_get_roundtrip(store):
31
+ store.setup("pass123")
32
+ store.set("appkey", "secret-value-123")
33
+ store.set("secretkey", "another-secret")
34
+ assert store.get("appkey") == "secret-value-123"
35
+ assert store.get("secretkey") == "another-secret"
36
+
37
+ def test_locked_access_raises(store):
38
+ store.setup("pass")
39
+ store.set("key", "val")
40
+ store.lock()
41
+ with pytest.raises(SecureStoreLocked):
42
+ store.get("key")
43
+ with pytest.raises(SecureStoreLocked):
44
+ store.set("key", "val2")
45
+
46
+ def test_get_nonexistent_returns_none(store):
47
+ store.setup("pass")
48
+ assert store.get("nonexistent") is None
49
+
50
+ def test_delete(store):
51
+ store.setup("pass")
52
+ store.set("key", "val")
53
+ store.delete("key")
54
+ assert store.get("key") is None
File without changes
File without changes