kiwoom-cli 2.2.0__tar.gz → 2.2.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-2.2.0 → kiwoom_cli-2.2.1}/PKG-INFO +1 -1
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/kiwoom_cli/__init__.py +1 -1
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/kiwoom_cli/client.py +6 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/kiwoom_cli/main.py +17 -1
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/kiwoom_cli.egg-info/PKG-INFO +1 -1
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/tests/test_auth_cli.py +56 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/LICENSE +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/README.md +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/kiwoom_cli/api_spec.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/kiwoom_cli/auth.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/kiwoom_cli/commands/__init__.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/kiwoom_cli/commands/_constants.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/kiwoom_cli/commands/account.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/kiwoom_cli/commands/dashboard.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/kiwoom_cli/commands/market.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/kiwoom_cli/commands/order.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/kiwoom_cli/commands/stock.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/kiwoom_cli/commands/stream.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/kiwoom_cli/commands/us/__init__.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/kiwoom_cli/commands/us/_constants.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/kiwoom_cli/commands/us/account_ops.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/kiwoom_cli/commands/us/detect.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/kiwoom_cli/commands/us/exchange.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/kiwoom_cli/commands/us/order_ops.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/kiwoom_cli/commands/us/stock_ops.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/kiwoom_cli/commands/watch.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/kiwoom_cli/config.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/kiwoom_cli/formatters.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/kiwoom_cli/output.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/kiwoom_cli/streaming.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/kiwoom_cli.egg-info/SOURCES.txt +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/kiwoom_cli.egg-info/dependency_links.txt +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/kiwoom_cli.egg-info/entry_points.txt +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/kiwoom_cli.egg-info/requires.txt +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/kiwoom_cli.egg-info/top_level.txt +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/pyproject.toml +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/setup.cfg +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/tests/test_account.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/tests/test_auth.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/tests/test_cli.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/tests/test_client.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/tests/test_credentials.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/tests/test_formatters.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/tests/test_formatters_property.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/tests/test_json_purity.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/tests/test_market.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/tests/test_migration.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/tests/test_order.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/tests/test_stock.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/tests/test_us.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.2.1}/tests/test_us_account.py +0 -0
|
@@ -24,6 +24,10 @@ class KiwoomAPIError(Exception):
|
|
|
24
24
|
super().__init__(f"[{code}] {msg}")
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
class KiwoomAuthError(Exception):
|
|
28
|
+
"""No access token available — the caller must authenticate first."""
|
|
29
|
+
|
|
30
|
+
|
|
27
31
|
class KiwoomClient:
|
|
28
32
|
"""Synchronous client for the Kiwoom REST API."""
|
|
29
33
|
|
|
@@ -80,6 +84,8 @@ class KiwoomClient:
|
|
|
80
84
|
next_key: str = "",
|
|
81
85
|
) -> tuple[dict[str, Any], dict[str, str]]:
|
|
82
86
|
"""Make a single API request. Returns (body_json, response_headers)."""
|
|
87
|
+
if not self.token:
|
|
88
|
+
raise KiwoomAuthError()
|
|
83
89
|
url_path = get_url(api_id)
|
|
84
90
|
headers = self._headers(api_id, cont_yn, next_key)
|
|
85
91
|
if self._should_spin():
|
|
@@ -7,9 +7,10 @@ import os
|
|
|
7
7
|
|
|
8
8
|
import click
|
|
9
9
|
import httpx
|
|
10
|
+
import keyring
|
|
10
11
|
|
|
11
12
|
from . import __version__, auth, config
|
|
12
|
-
from .client import KiwoomClient, KiwoomAPIError
|
|
13
|
+
from .client import KiwoomClient, KiwoomAPIError, KiwoomAuthError
|
|
13
14
|
from .commands.account import account
|
|
14
15
|
from .commands.dashboard import dashboard
|
|
15
16
|
from .commands.market import market
|
|
@@ -45,6 +46,21 @@ class KiwoomGroup(click.Group):
|
|
|
45
46
|
else:
|
|
46
47
|
console.print(f"[red]API 오류:[/] {e}")
|
|
47
48
|
ctx.exit(EXIT_API)
|
|
49
|
+
except KiwoomAuthError:
|
|
50
|
+
fmt = ctx.obj.get("format", "table") if ctx.obj else "table"
|
|
51
|
+
msg = "토큰이 없습니다. 'kiwoom auth login'으로 발급하세요."
|
|
52
|
+
if fmt == "json":
|
|
53
|
+
click.echo(json.dumps({"error": msg, "code": "auth_required"}, ensure_ascii=False))
|
|
54
|
+
else:
|
|
55
|
+
console.print("[red]인증 필요:[/] 토큰이 없습니다. [dim]kiwoom auth login[/]")
|
|
56
|
+
ctx.exit(EXIT_AUTH)
|
|
57
|
+
except keyring.errors.KeyringError:
|
|
58
|
+
console.print("[red]키체인 오류:[/] OS 키체인에 접근할 수 없습니다 (잠김 또는 비대화형 세션).")
|
|
59
|
+
console.print(
|
|
60
|
+
"[dim]키체인 없는 환경에서는 본인 터미널에서 토큰을 발급한 뒤 "
|
|
61
|
+
"KIWOOM_TOKEN 환경변수로 전달하세요. (README '샌드박스 환경' 참고)[/]"
|
|
62
|
+
)
|
|
63
|
+
ctx.exit(EXIT_INPUT)
|
|
48
64
|
except httpx.HTTPStatusError as e:
|
|
49
65
|
if e.response.status_code == 401:
|
|
50
66
|
console.print("[red]인증 오류:[/] 토큰이 만료되었습니다. [dim]kiwoom auth login[/]")
|
|
@@ -234,3 +234,59 @@ def test_auth_status_reflects_env_token_with_broken_keyring(monkeypatch):
|
|
|
234
234
|
result = runner.invoke(cli, ["auth", "status"])
|
|
235
235
|
assert result.exit_code == 0
|
|
236
236
|
assert "KIWOOM_TOKEN" in result.output
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# ── 키체인 쓰기 실패 / 토큰 부재 처리 ─────────────────────
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def test_config_setup_keychain_write_failure_friendly_error(monkeypatch, mem_keyring):
|
|
243
|
+
"""키체인 쓰기 불가(-25308 등): traceback 대신 친절한 안내 + exit 1."""
|
|
244
|
+
|
|
245
|
+
def _locked(svc, key, val):
|
|
246
|
+
raise keyring.errors.KeyringError("errSecInteractionNotAllowed (-25308)")
|
|
247
|
+
|
|
248
|
+
monkeypatch.setattr(keyring, "set_password", _locked)
|
|
249
|
+
runner = CliRunner()
|
|
250
|
+
result = runner.invoke(
|
|
251
|
+
cli, ["config", "setup"], input="ak\nsk\nmock\n\n\n"
|
|
252
|
+
)
|
|
253
|
+
assert result.exit_code == 1, result.output
|
|
254
|
+
# 예외가 새어나오지 않고 CLI가 스스로 종료해야 한다
|
|
255
|
+
assert result.exception is None or isinstance(result.exception, SystemExit)
|
|
256
|
+
combined = result.output + result.stderr
|
|
257
|
+
assert "키체인" in combined
|
|
258
|
+
assert "KIWOOM_TOKEN" in combined
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def test_missing_token_exits_3_before_any_request(monkeypatch, mem_keyring):
|
|
262
|
+
"""토큰이 전혀 없으면 요청을 보내지 않고 exit 3 + auth login 힌트."""
|
|
263
|
+
import httpx
|
|
264
|
+
|
|
265
|
+
monkeypatch.delenv("KIWOOM_TOKEN", raising=False)
|
|
266
|
+
|
|
267
|
+
def _no_network(*args, **kwargs):
|
|
268
|
+
raise AssertionError("token 없이 HTTP 요청이 나가면 안 된다")
|
|
269
|
+
|
|
270
|
+
monkeypatch.setattr(httpx.Client, "post", _no_network)
|
|
271
|
+
runner = CliRunner()
|
|
272
|
+
result = runner.invoke(cli, ["stock", "info", "005930"])
|
|
273
|
+
assert result.exit_code == 3, result.output
|
|
274
|
+
assert "auth login" in result.output + result.stderr
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def test_missing_token_json_mode_single_json_error_doc(monkeypatch, mem_keyring):
|
|
278
|
+
"""-f json + 토큰 부재: stdout은 단일 JSON 에러 문서, exit 3."""
|
|
279
|
+
import json as _json
|
|
280
|
+
|
|
281
|
+
import httpx
|
|
282
|
+
|
|
283
|
+
monkeypatch.delenv("KIWOOM_TOKEN", raising=False)
|
|
284
|
+
monkeypatch.setattr(
|
|
285
|
+
httpx.Client, "post",
|
|
286
|
+
lambda *a, **k: (_ for _ in ()).throw(AssertionError("no request expected")),
|
|
287
|
+
)
|
|
288
|
+
runner = CliRunner()
|
|
289
|
+
result = runner.invoke(cli, ["-f", "json", "stock", "info", "005930"])
|
|
290
|
+
assert result.exit_code == 3, result.output
|
|
291
|
+
doc = _json.loads(result.stdout)
|
|
292
|
+
assert "error" in doc
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|