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.
- {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/PKG-INFO +3 -4
- {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/README.md +0 -2
- {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli/__init__.py +1 -1
- {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli/commands/order.py +76 -37
- {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli/formatters.py +4 -3
- {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli/main.py +12 -10
- {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli/secure_store.py +9 -25
- {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli.egg-info/PKG-INFO +3 -4
- {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli.egg-info/SOURCES.txt +2 -1
- {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli.egg-info/requires.txt +1 -0
- {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/pyproject.toml +6 -2
- {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/tests/test_cli.py +38 -1
- kiwoom_cli-1.0.1/tests/test_secure_store.py +54 -0
- {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/LICENSE +0 -0
- {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli/api_spec.py +0 -0
- {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli/auth.py +0 -0
- {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli/client.py +0 -0
- {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli/commands/__init__.py +0 -0
- {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli/commands/_constants.py +0 -0
- {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli/commands/account.py +0 -0
- {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli/commands/dashboard.py +0 -0
- {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli/commands/market.py +0 -0
- {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli/commands/stock.py +0 -0
- {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli/commands/stream.py +0 -0
- {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli/commands/watch.py +0 -0
- {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli/config.py +0 -0
- {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli/output.py +0 -0
- {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli/streaming.py +0 -0
- {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli.egg-info/dependency_links.txt +0 -0
- {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli.egg-info/entry_points.txt +0 -0
- {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/kiwoom_cli.egg-info/top_level.txt +0 -0
- {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/setup.cfg +0 -0
- {kiwoom_cli-0.7.0 → kiwoom_cli-1.0.1}/tests/test_client.py +0 -0
- {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.
|
|
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 ::
|
|
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,6 +1,7 @@
|
|
|
1
1
|
"""Order management commands.
|
|
2
2
|
|
|
3
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
409
|
-
def gold_balance(confirm: bool):
|
|
445
|
+
def gold_balance():
|
|
410
446
|
"""금현물 잔고확인 (kt50020).
|
|
411
447
|
|
|
412
|
-
예: kiwoom order gold balance
|
|
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
|
-
|
|
421
|
-
def gold_deposit(confirm: bool):
|
|
456
|
+
def gold_deposit():
|
|
422
457
|
"""금현물 예수금 (kt50021).
|
|
423
458
|
|
|
424
|
-
예: kiwoom order gold deposit
|
|
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
|
-
|
|
433
|
-
def gold_executions_all(confirm: bool):
|
|
467
|
+
def gold_executions_all():
|
|
434
468
|
"""금현물 주문체결전체조회 (kt50030).
|
|
435
469
|
|
|
436
|
-
예: kiwoom order gold executions-all
|
|
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
|
-
|
|
445
|
-
def gold_executions(confirm: bool):
|
|
478
|
+
def gold_executions():
|
|
446
479
|
"""금현물 주문체결조회 (kt50031).
|
|
447
480
|
|
|
448
|
-
예: kiwoom order gold executions
|
|
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
|
-
|
|
457
|
-
def gold_history(confirm: bool):
|
|
489
|
+
def gold_history():
|
|
458
490
|
"""금현물 거래내역조회 (kt50032).
|
|
459
491
|
|
|
460
|
-
예: kiwoom order gold history
|
|
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
|
-
|
|
469
|
-
def gold_pending(confirm: bool):
|
|
500
|
+
def gold_pending():
|
|
470
501
|
"""금현물 미체결조회 (kt50075).
|
|
471
502
|
|
|
472
|
-
예: kiwoom order gold pending
|
|
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
|
-
|
|
491
|
-
def condition_list(confirm: bool):
|
|
521
|
+
def condition_list():
|
|
492
522
|
"""조건검색 목록조회 (ka10171).
|
|
493
523
|
|
|
494
|
-
예: kiwoom order condition list
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
55
|
-
if
|
|
54
|
+
s = value.strip()
|
|
55
|
+
if s.startswith("-"):
|
|
56
56
|
return "blue"
|
|
57
|
-
|
|
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="도메인
|
|
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
|
|
117
|
-
config.store.setup(password)
|
|
118
|
-
else:
|
|
116
|
+
if config.store.is_initialized:
|
|
119
117
|
if not config.store.unlock(password):
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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-
|
|
39
|
-
key. The encryption key never touches disk — it exists
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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 ::
|
|
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 키체인 |
|
|
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "kiwoom-cli"
|
|
7
|
-
|
|
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 ::
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|