kiwoom-cli 2.2.0__tar.gz → 2.3.0__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.3.0}/PKG-INFO +51 -3
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/README.md +50 -2
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/kiwoom_cli/__init__.py +1 -1
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/kiwoom_cli/client.py +16 -0
- kiwoom_cli-2.3.0/kiwoom_cli/envelope.py +132 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/kiwoom_cli/formatters.py +3 -3
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/kiwoom_cli/main.py +113 -34
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/kiwoom_cli.egg-info/PKG-INFO +51 -3
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/kiwoom_cli.egg-info/SOURCES.txt +2 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/tests/test_auth_cli.py +60 -4
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/tests/test_cli.py +2 -1
- kiwoom_cli-2.3.0/tests/test_envelope.py +375 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/tests/test_formatters.py +8 -8
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/tests/test_json_purity.py +5 -3
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/tests/test_us_account.py +6 -6
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/LICENSE +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/kiwoom_cli/api_spec.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/kiwoom_cli/auth.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/kiwoom_cli/commands/__init__.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/kiwoom_cli/commands/_constants.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/kiwoom_cli/commands/account.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/kiwoom_cli/commands/dashboard.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/kiwoom_cli/commands/market.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/kiwoom_cli/commands/order.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/kiwoom_cli/commands/stock.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/kiwoom_cli/commands/stream.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/kiwoom_cli/commands/us/__init__.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/kiwoom_cli/commands/us/_constants.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/kiwoom_cli/commands/us/account_ops.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/kiwoom_cli/commands/us/detect.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/kiwoom_cli/commands/us/exchange.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/kiwoom_cli/commands/us/order_ops.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/kiwoom_cli/commands/us/stock_ops.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/kiwoom_cli/commands/watch.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/kiwoom_cli/config.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/kiwoom_cli/output.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/kiwoom_cli/streaming.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/kiwoom_cli.egg-info/dependency_links.txt +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/kiwoom_cli.egg-info/entry_points.txt +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/kiwoom_cli.egg-info/requires.txt +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/kiwoom_cli.egg-info/top_level.txt +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/pyproject.toml +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/setup.cfg +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/tests/test_account.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/tests/test_auth.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/tests/test_client.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/tests/test_credentials.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/tests/test_formatters_property.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/tests/test_market.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/tests/test_migration.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/tests/test_order.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/tests/test_stock.py +0 -0
- {kiwoom_cli-2.2.0 → kiwoom_cli-2.3.0}/tests/test_us.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kiwoom-cli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: User-friendly CLI for the Kiwoom Securities REST API (키움증권)
|
|
5
5
|
License: LicenseRef-kiwoom-cli-Source-Available-1.0
|
|
6
6
|
Project-URL: Homepage, https://github.com/gejyn14/kiwoom-cli
|
|
@@ -512,13 +512,61 @@ kiwoom --no-color stock info 005930 # 색상 없이 (파일 저장용)
|
|
|
512
512
|
```
|
|
513
513
|
|
|
514
514
|
```bash
|
|
515
|
-
# jq로 필터링
|
|
516
|
-
kiwoom -f json market rank volume | jq '.[].stk_nm'
|
|
515
|
+
# jq로 필터링 (본문은 .data 아래)
|
|
516
|
+
kiwoom -f json market rank volume | jq '.data[].stk_nm'
|
|
517
517
|
|
|
518
518
|
# CSV를 파일로 저장
|
|
519
519
|
kiwoom -f csv stock daily 005930 > samsung_daily.csv
|
|
520
520
|
```
|
|
521
521
|
|
|
522
|
+
### JSON 응답 envelope (v1)
|
|
523
|
+
|
|
524
|
+
`-f json`의 모든 응답은 성공/실패 모두 하나의 안정적인 envelope로 감쌉니다. table/csv 모드는 그대로입니다.
|
|
525
|
+
|
|
526
|
+
```json
|
|
527
|
+
{
|
|
528
|
+
"ok": true,
|
|
529
|
+
"schema": "v1",
|
|
530
|
+
"data": { "stk_nm": "삼성전자", "cur_prc": "+70000" },
|
|
531
|
+
"meta": { "profile": "default", "env": "prod", "cont": { "next_key": "..." } },
|
|
532
|
+
"error": null
|
|
533
|
+
}
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
- `data` — 기존 응답 본문 그대로 (`return_code`/`return_msg`만 제거)
|
|
537
|
+
- `meta.profile` / `meta.env` — 해석된 프로필과 도메인(`prod`/`mock`)
|
|
538
|
+
- `meta.cont` — 연속조회 커서. 값이 있으면 다음 페이지가 존재: `kiwoom api <api_id> <body> --next-key <meta.cont.next_key>`
|
|
539
|
+
- 실패 시 `ok: false`, `data: null`이고 `error`에 안정적인 코드가 담깁니다:
|
|
540
|
+
|
|
541
|
+
```json
|
|
542
|
+
{ "code": "TOKEN_EXPIRED", "retryable": false, "message": "Token이 유효하지 않습니다", "upstream_code": 8005 }
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
`error.code`는 키움 오류코드/HTTP 상태를 분류한 stable enum입니다 — 에이전트는 메시지 문자열 대신 이 코드로 분기하세요:
|
|
546
|
+
|
|
547
|
+
| code | retryable | 의미 (upstream) |
|
|
548
|
+
| --- | :---: | --- |
|
|
549
|
+
| `INVALID_INPUT` | X | 입력 값/필수 파라미터 오류 (2, 1511, 1512, 1517) |
|
|
550
|
+
| `INVALID_API` | X | 잘못된 API ID (1501, 1504, 1505) |
|
|
551
|
+
| `NOT_FOUND` | X | 시장/종목 정보 없음 (1901, 1902) |
|
|
552
|
+
| `AUTH_REQUIRED` | X | 인증 헤더/토큰 누락 (1513~1516, 토큰 미보유 시 로컬 감지) |
|
|
553
|
+
| `TOKEN_EXPIRED` | X | 토큰 무효·만료 (8005, HTTP 401) |
|
|
554
|
+
| `TOKEN_ISSUE_FAILED` | X | 토큰 발급 실패 (8003, 8006, 8009, 8011, 8012) |
|
|
555
|
+
| `TOKEN_REVOKE_FAILED` | X | 토큰 폐기 실패 (8015, 8016) |
|
|
556
|
+
| `INVALID_CREDENTIALS` | X | appkey/secretkey 검증 실패 (8001, 8002, 8020) |
|
|
557
|
+
| `IP_MISMATCH` | X | 발급 IP와 요청 IP 불일치 (8010) |
|
|
558
|
+
| `ENV_MISMATCH` | X | 실전/모의 구분 불일치 (8030, 8031) |
|
|
559
|
+
| `DEVICE_AUTH_FAILED` | X | 단말기 인증 실패 (8040, 8050, 8103) |
|
|
560
|
+
| `RATE_LIMITED` | 1700·429는 O | 요청 개수 초과 (1700, HTTP 429) / 재귀 호출 제한 (1687) |
|
|
561
|
+
| `NETWORK_ERROR` | O | API 서버 연결 실패 |
|
|
562
|
+
| `KEYCHAIN_UNAVAILABLE` | X | OS 키체인 접근 불가 (샌드박스/CI) |
|
|
563
|
+
| `NOT_CONFIGURED` | X | `config setup` 미실행 (CLI 로컬 감지) |
|
|
564
|
+
| `UPSTREAM_ERROR` | 5xx·1999는 O | 미분류 업스트림 오류 (기본값) |
|
|
565
|
+
|
|
566
|
+
CLI 수준의 인자/옵션 오류(잘못된 값, 인자 누락 등)도 json 모드에서는 `INVALID_INPUT` envelope로 출력됩니다 (`upstream_code: null`, exit 1). `kiwoom api --raw`는 json 모드에서도 envelope로 감싸되 `data`에 응답 원본을 그대로(`return_code` 포함) 담습니다. `auth login`은 json 모드에서 `{profile, token_storage, saved, token}`을 반환하며, `token` 원문은 env 모드에서만 포함됩니다.
|
|
567
|
+
|
|
568
|
+
exit code 계약은 그대로입니다 (0=성공, 1=입력오류, 2=API오류, 3=인증필요).
|
|
569
|
+
|
|
522
570
|
## Shell 자동완성
|
|
523
571
|
|
|
524
572
|
```bash
|
|
@@ -475,13 +475,61 @@ kiwoom --no-color stock info 005930 # 색상 없이 (파일 저장용)
|
|
|
475
475
|
```
|
|
476
476
|
|
|
477
477
|
```bash
|
|
478
|
-
# jq로 필터링
|
|
479
|
-
kiwoom -f json market rank volume | jq '.[].stk_nm'
|
|
478
|
+
# jq로 필터링 (본문은 .data 아래)
|
|
479
|
+
kiwoom -f json market rank volume | jq '.data[].stk_nm'
|
|
480
480
|
|
|
481
481
|
# CSV를 파일로 저장
|
|
482
482
|
kiwoom -f csv stock daily 005930 > samsung_daily.csv
|
|
483
483
|
```
|
|
484
484
|
|
|
485
|
+
### JSON 응답 envelope (v1)
|
|
486
|
+
|
|
487
|
+
`-f json`의 모든 응답은 성공/실패 모두 하나의 안정적인 envelope로 감쌉니다. table/csv 모드는 그대로입니다.
|
|
488
|
+
|
|
489
|
+
```json
|
|
490
|
+
{
|
|
491
|
+
"ok": true,
|
|
492
|
+
"schema": "v1",
|
|
493
|
+
"data": { "stk_nm": "삼성전자", "cur_prc": "+70000" },
|
|
494
|
+
"meta": { "profile": "default", "env": "prod", "cont": { "next_key": "..." } },
|
|
495
|
+
"error": null
|
|
496
|
+
}
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
- `data` — 기존 응답 본문 그대로 (`return_code`/`return_msg`만 제거)
|
|
500
|
+
- `meta.profile` / `meta.env` — 해석된 프로필과 도메인(`prod`/`mock`)
|
|
501
|
+
- `meta.cont` — 연속조회 커서. 값이 있으면 다음 페이지가 존재: `kiwoom api <api_id> <body> --next-key <meta.cont.next_key>`
|
|
502
|
+
- 실패 시 `ok: false`, `data: null`이고 `error`에 안정적인 코드가 담깁니다:
|
|
503
|
+
|
|
504
|
+
```json
|
|
505
|
+
{ "code": "TOKEN_EXPIRED", "retryable": false, "message": "Token이 유효하지 않습니다", "upstream_code": 8005 }
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
`error.code`는 키움 오류코드/HTTP 상태를 분류한 stable enum입니다 — 에이전트는 메시지 문자열 대신 이 코드로 분기하세요:
|
|
509
|
+
|
|
510
|
+
| code | retryable | 의미 (upstream) |
|
|
511
|
+
| --- | :---: | --- |
|
|
512
|
+
| `INVALID_INPUT` | X | 입력 값/필수 파라미터 오류 (2, 1511, 1512, 1517) |
|
|
513
|
+
| `INVALID_API` | X | 잘못된 API ID (1501, 1504, 1505) |
|
|
514
|
+
| `NOT_FOUND` | X | 시장/종목 정보 없음 (1901, 1902) |
|
|
515
|
+
| `AUTH_REQUIRED` | X | 인증 헤더/토큰 누락 (1513~1516, 토큰 미보유 시 로컬 감지) |
|
|
516
|
+
| `TOKEN_EXPIRED` | X | 토큰 무효·만료 (8005, HTTP 401) |
|
|
517
|
+
| `TOKEN_ISSUE_FAILED` | X | 토큰 발급 실패 (8003, 8006, 8009, 8011, 8012) |
|
|
518
|
+
| `TOKEN_REVOKE_FAILED` | X | 토큰 폐기 실패 (8015, 8016) |
|
|
519
|
+
| `INVALID_CREDENTIALS` | X | appkey/secretkey 검증 실패 (8001, 8002, 8020) |
|
|
520
|
+
| `IP_MISMATCH` | X | 발급 IP와 요청 IP 불일치 (8010) |
|
|
521
|
+
| `ENV_MISMATCH` | X | 실전/모의 구분 불일치 (8030, 8031) |
|
|
522
|
+
| `DEVICE_AUTH_FAILED` | X | 단말기 인증 실패 (8040, 8050, 8103) |
|
|
523
|
+
| `RATE_LIMITED` | 1700·429는 O | 요청 개수 초과 (1700, HTTP 429) / 재귀 호출 제한 (1687) |
|
|
524
|
+
| `NETWORK_ERROR` | O | API 서버 연결 실패 |
|
|
525
|
+
| `KEYCHAIN_UNAVAILABLE` | X | OS 키체인 접근 불가 (샌드박스/CI) |
|
|
526
|
+
| `NOT_CONFIGURED` | X | `config setup` 미실행 (CLI 로컬 감지) |
|
|
527
|
+
| `UPSTREAM_ERROR` | 5xx·1999는 O | 미분류 업스트림 오류 (기본값) |
|
|
528
|
+
|
|
529
|
+
CLI 수준의 인자/옵션 오류(잘못된 값, 인자 누락 등)도 json 모드에서는 `INVALID_INPUT` envelope로 출력됩니다 (`upstream_code: null`, exit 1). `kiwoom api --raw`는 json 모드에서도 envelope로 감싸되 `data`에 응답 원본을 그대로(`return_code` 포함) 담습니다. `auth login`은 json 모드에서 `{profile, token_storage, saved, token}`을 반환하며, `token` 원문은 env 모드에서만 포함됩니다.
|
|
530
|
+
|
|
531
|
+
exit code 계약은 그대로입니다 (0=성공, 1=입력오류, 2=API오류, 3=인증필요).
|
|
532
|
+
|
|
485
533
|
## Shell 자동완성
|
|
486
534
|
|
|
487
535
|
```bash
|
|
@@ -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():
|
|
@@ -98,6 +104,16 @@ class KiwoomClient:
|
|
|
98
104
|
"cont-yn": resp.headers.get("cont-yn", ""),
|
|
99
105
|
"next-key": resp.headers.get("next-key", ""),
|
|
100
106
|
}
|
|
107
|
+
|
|
108
|
+
# 연속조회 커서를 Click 컨텍스트에 보관 → json envelope의 meta.cont로 노출
|
|
109
|
+
# (라이브러리로 쓰일 때는 컨텍스트가 없으므로 조용히 건너뜀)
|
|
110
|
+
ctx = click.get_current_context(silent=True)
|
|
111
|
+
if ctx is not None and isinstance(ctx.obj, dict):
|
|
112
|
+
if resp_headers["cont-yn"] == "Y" and resp_headers["next-key"]:
|
|
113
|
+
ctx.obj["last_cont"] = {"next_key": resp_headers["next-key"]}
|
|
114
|
+
else:
|
|
115
|
+
ctx.obj["last_cont"] = None
|
|
116
|
+
|
|
101
117
|
return data, resp_headers
|
|
102
118
|
|
|
103
119
|
def request_all(
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Stable JSON response envelope (v1) for `-f json` output.
|
|
2
|
+
|
|
3
|
+
성공/실패 모두 하나의 envelope로 감쌉니다:
|
|
4
|
+
{"ok": bool, "schema": "v1", "data": ..., "meta": {...}, "error": ...}
|
|
5
|
+
|
|
6
|
+
에러는 안정적인 enum 코드로 분류되어 자동화/에이전트가 upstream 메시지를
|
|
7
|
+
파싱하지 않고도 분기할 수 있습니다. csv/table 모드에는 적용되지 않습니다.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
import click
|
|
17
|
+
|
|
18
|
+
from . import config
|
|
19
|
+
|
|
20
|
+
SCHEMA = "v1"
|
|
21
|
+
|
|
22
|
+
# 키움 REST API 공식 오류코드 (docs/키움 REST API 문서.xlsx '오류코드' 시트)
|
|
23
|
+
# → (stable_code, retryable). 미등록 코드는 ("UPSTREAM_ERROR", False).
|
|
24
|
+
CODE_MAP: dict[int, tuple[str, bool]] = {
|
|
25
|
+
# API 라우팅
|
|
26
|
+
1501: ("INVALID_API", False), # API ID Null/없음
|
|
27
|
+
1504: ("INVALID_API", False), # URI가 지원하지 않는 API ID
|
|
28
|
+
1505: ("INVALID_API", False), # 존재하지 않는 API ID
|
|
29
|
+
# 입력 검증
|
|
30
|
+
2: ("INVALID_INPUT", False), # 입력 값 오류 (실측)
|
|
31
|
+
1511: ("INVALID_INPUT", False), # 필수 입력 누락
|
|
32
|
+
1512: ("INVALID_INPUT", False), # Http header 값 누락/불가독
|
|
33
|
+
1517: ("INVALID_INPUT", False), # 입력 값 형식 오류
|
|
34
|
+
# 인증 헤더/토큰
|
|
35
|
+
1513: ("AUTH_REQUIRED", False), # authorization 필드 없음
|
|
36
|
+
1514: ("AUTH_REQUIRED", False), # authorization 형식 오류
|
|
37
|
+
1515: ("AUTH_REQUIRED", False), # Grant Type 형식 오류
|
|
38
|
+
1516: ("AUTH_REQUIRED", False), # Token 미정의
|
|
39
|
+
8005: ("TOKEN_EXPIRED", False), # Token 유효하지 않음
|
|
40
|
+
# 호출 제한
|
|
41
|
+
1687: ("RATE_LIMITED", False), # 재귀 호출 제한 (재시도 무의미)
|
|
42
|
+
1700: ("RATE_LIMITED", True), # 허용 요청 개수 초과
|
|
43
|
+
# 종목/시장 조회
|
|
44
|
+
1901: ("NOT_FOUND", False), # 시장 코드 없음
|
|
45
|
+
1902: ("NOT_FOUND", False), # 종목 정보 없음
|
|
46
|
+
# 서버 오류
|
|
47
|
+
1999: ("UPSTREAM_ERROR", True), # 예기치 못한 에러
|
|
48
|
+
# 앱키/토큰 발급·폐기
|
|
49
|
+
8001: ("INVALID_CREDENTIALS", False), # App/Secret Key 검증 실패
|
|
50
|
+
8002: ("INVALID_CREDENTIALS", False),
|
|
51
|
+
8020: ("INVALID_CREDENTIALS", False), # appkey/secretkey 미입력
|
|
52
|
+
8003: ("TOKEN_ISSUE_FAILED", False), # Access Token 조회 실패
|
|
53
|
+
8006: ("TOKEN_ISSUE_FAILED", False),
|
|
54
|
+
8009: ("TOKEN_ISSUE_FAILED", False),
|
|
55
|
+
8011: ("TOKEN_ISSUE_FAILED", False),
|
|
56
|
+
8012: ("TOKEN_ISSUE_FAILED", False),
|
|
57
|
+
8015: ("TOKEN_REVOKE_FAILED", False),
|
|
58
|
+
8016: ("TOKEN_REVOKE_FAILED", False),
|
|
59
|
+
# 환경/단말 제약
|
|
60
|
+
8010: ("IP_MISMATCH", False), # 발급 IP와 요청 IP 불일치
|
|
61
|
+
8030: ("ENV_MISMATCH", False), # 실전/모의 구분 불일치 (Appkey)
|
|
62
|
+
8031: ("ENV_MISMATCH", False), # 실전/모의 구분 불일치 (Token)
|
|
63
|
+
8040: ("DEVICE_AUTH_FAILED", False),
|
|
64
|
+
8050: ("DEVICE_AUTH_FAILED", False),
|
|
65
|
+
8103: ("DEVICE_AUTH_FAILED", False),
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
HTTP_MAP: dict[int, tuple[str, bool]] = {
|
|
69
|
+
401: ("TOKEN_EXPIRED", False),
|
|
70
|
+
429: ("RATE_LIMITED", True),
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
DEFAULT = ("UPSTREAM_ERROR", False)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def classify(upstream_code: int | None = None, http_status: int | None = None) -> tuple[str, bool]:
|
|
77
|
+
"""(stable_code, retryable)로 분류. http_status가 있으면 우선."""
|
|
78
|
+
if http_status is not None:
|
|
79
|
+
if http_status in HTTP_MAP:
|
|
80
|
+
return HTTP_MAP[http_status]
|
|
81
|
+
if 500 <= http_status < 600:
|
|
82
|
+
return ("UPSTREAM_ERROR", True)
|
|
83
|
+
return DEFAULT
|
|
84
|
+
if upstream_code is not None and upstream_code in CODE_MAP:
|
|
85
|
+
return CODE_MAP[upstream_code]
|
|
86
|
+
return DEFAULT
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def build_meta() -> dict[str, Any]:
|
|
90
|
+
ctx = click.get_current_context(silent=True)
|
|
91
|
+
obj = ctx.obj if ctx is not None and isinstance(ctx.obj, dict) else {}
|
|
92
|
+
profile = config.resolve_profile(obj.get("profile"))
|
|
93
|
+
env = os.environ.get("KIWOOM_DOMAIN")
|
|
94
|
+
if env not in config.DOMAINS:
|
|
95
|
+
cfg = config.load_config()
|
|
96
|
+
env = cfg.get("profiles", {}).get(profile, {}).get("domain", "mock")
|
|
97
|
+
if env not in config.DOMAINS:
|
|
98
|
+
env = "mock"
|
|
99
|
+
return {"profile": profile, "env": env, "cont": obj.get("last_cont") or None}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def emit(data: Any = None, *, error: dict[str, Any] | None = None) -> None:
|
|
103
|
+
"""Envelope 전체를 단일 JSON 문서로 stdout에 출력."""
|
|
104
|
+
doc = {
|
|
105
|
+
"ok": error is None,
|
|
106
|
+
"schema": SCHEMA,
|
|
107
|
+
"data": data,
|
|
108
|
+
"meta": build_meta(),
|
|
109
|
+
"error": error,
|
|
110
|
+
}
|
|
111
|
+
click.echo(json.dumps(doc, ensure_ascii=False, indent=2))
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def error_body(
|
|
115
|
+
message: str,
|
|
116
|
+
*,
|
|
117
|
+
upstream_code: int | None = None,
|
|
118
|
+
http_status: int | None = None,
|
|
119
|
+
code: str | None = None,
|
|
120
|
+
retryable: bool | None = None,
|
|
121
|
+
) -> dict[str, Any]:
|
|
122
|
+
"""에러 dict 구성. code/retryable을 직접 주지 않으면 classify()로 결정."""
|
|
123
|
+
if code is None or retryable is None:
|
|
124
|
+
c, r = classify(upstream_code=upstream_code, http_status=http_status)
|
|
125
|
+
code = code if code is not None else c
|
|
126
|
+
retryable = retryable if retryable is not None else r
|
|
127
|
+
return {
|
|
128
|
+
"code": code,
|
|
129
|
+
"retryable": retryable,
|
|
130
|
+
"message": message,
|
|
131
|
+
"upstream_code": upstream_code if upstream_code is not None else http_status,
|
|
132
|
+
}
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import csv
|
|
6
|
-
import json
|
|
7
6
|
import sys
|
|
8
7
|
from typing import Any
|
|
9
8
|
|
|
@@ -12,6 +11,7 @@ from rich.panel import Panel
|
|
|
12
11
|
from rich.table import Table
|
|
13
12
|
from rich.text import Text
|
|
14
13
|
|
|
14
|
+
from . import envelope
|
|
15
15
|
from .output import console, err_console
|
|
16
16
|
|
|
17
17
|
|
|
@@ -35,11 +35,11 @@ def human(renderable: Any) -> None:
|
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
def _output_json(data: Any) -> None:
|
|
38
|
-
"""Write data as JSON to stdout."""
|
|
38
|
+
"""Write data as an enveloped JSON document to stdout."""
|
|
39
39
|
clean = data
|
|
40
40
|
if isinstance(data, dict):
|
|
41
41
|
clean = {k: v for k, v in data.items() if k not in ("return_code", "return_msg")}
|
|
42
|
-
|
|
42
|
+
envelope.emit(data=clean)
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
def _output_csv(rows: list[dict], keys: list[str] | None = None) -> None:
|
|
@@ -7,9 +7,10 @@ import os
|
|
|
7
7
|
|
|
8
8
|
import click
|
|
9
9
|
import httpx
|
|
10
|
+
import keyring
|
|
10
11
|
|
|
11
|
-
from . import __version__, auth, config
|
|
12
|
-
from .client import KiwoomClient, KiwoomAPIError
|
|
12
|
+
from . import __version__, auth, config, envelope
|
|
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
|
|
@@ -35,26 +36,68 @@ click.exceptions.UsageError.exit_code = EXIT_INPUT
|
|
|
35
36
|
class KiwoomGroup(click.Group):
|
|
36
37
|
"""Custom group that catches API/network errors globally."""
|
|
37
38
|
|
|
39
|
+
@staticmethod
|
|
40
|
+
def _json_mode(ctx) -> bool:
|
|
41
|
+
return (ctx.obj.get("format", "table") if ctx.obj else "table") == "json"
|
|
42
|
+
|
|
38
43
|
def invoke(self, ctx):
|
|
39
44
|
try:
|
|
40
45
|
return super().invoke(ctx)
|
|
41
46
|
except KiwoomAPIError as e:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
click.echo(json.dumps({"error": e.msg, "code": e.code}, ensure_ascii=False))
|
|
47
|
+
if self._json_mode(ctx):
|
|
48
|
+
envelope.emit(error=envelope.error_body(e.msg, upstream_code=e.code))
|
|
45
49
|
else:
|
|
46
50
|
console.print(f"[red]API 오류:[/] {e}")
|
|
47
51
|
ctx.exit(EXIT_API)
|
|
52
|
+
except KiwoomAuthError:
|
|
53
|
+
if self._json_mode(ctx):
|
|
54
|
+
envelope.emit(error=envelope.error_body(
|
|
55
|
+
"토큰이 없습니다. 'kiwoom auth login'으로 발급하세요.",
|
|
56
|
+
code="AUTH_REQUIRED", retryable=False,
|
|
57
|
+
))
|
|
58
|
+
else:
|
|
59
|
+
console.print("[red]인증 필요:[/] 토큰이 없습니다. [dim]kiwoom auth login[/]")
|
|
60
|
+
ctx.exit(EXIT_AUTH)
|
|
61
|
+
except keyring.errors.KeyringError:
|
|
62
|
+
if self._json_mode(ctx):
|
|
63
|
+
envelope.emit(error=envelope.error_body(
|
|
64
|
+
"OS 키체인에 접근할 수 없습니다. KIWOOM_TOKEN 환경변수를 사용하세요.",
|
|
65
|
+
code="KEYCHAIN_UNAVAILABLE", retryable=False,
|
|
66
|
+
))
|
|
67
|
+
else:
|
|
68
|
+
console.print("[red]키체인 오류:[/] OS 키체인에 접근할 수 없습니다 (잠김 또는 비대화형 세션).")
|
|
69
|
+
console.print(
|
|
70
|
+
"[dim]키체인 없는 환경에서는 본인 터미널에서 토큰을 발급한 뒤 "
|
|
71
|
+
"KIWOOM_TOKEN 환경변수로 전달하세요. (README '샌드박스 환경' 참고)[/]"
|
|
72
|
+
)
|
|
73
|
+
ctx.exit(EXIT_INPUT)
|
|
48
74
|
except httpx.HTTPStatusError as e:
|
|
49
|
-
|
|
75
|
+
status = e.response.status_code
|
|
76
|
+
if self._json_mode(ctx):
|
|
77
|
+
envelope.emit(error=envelope.error_body(f"HTTP {status}", http_status=status))
|
|
78
|
+
elif status == 401:
|
|
50
79
|
console.print("[red]인증 오류:[/] 토큰이 만료되었습니다. [dim]kiwoom auth login[/]")
|
|
51
|
-
ctx.exit(EXIT_AUTH)
|
|
52
80
|
else:
|
|
53
|
-
console.print(f"[red]HTTP 오류:[/] {
|
|
54
|
-
|
|
81
|
+
console.print(f"[red]HTTP 오류:[/] {status}")
|
|
82
|
+
ctx.exit(EXIT_AUTH if status == 401 else EXIT_API)
|
|
55
83
|
except httpx.ConnectError:
|
|
56
|
-
|
|
84
|
+
if self._json_mode(ctx):
|
|
85
|
+
envelope.emit(error=envelope.error_body(
|
|
86
|
+
"API 서버에 연결할 수 없습니다. 도메인을 확인하세요.",
|
|
87
|
+
code="NETWORK_ERROR", retryable=True,
|
|
88
|
+
))
|
|
89
|
+
else:
|
|
90
|
+
console.print("[red]연결 오류:[/] API 서버에 연결할 수 없습니다. 도메인을 확인하세요.")
|
|
57
91
|
ctx.exit(EXIT_API)
|
|
92
|
+
except click.ClickException as e:
|
|
93
|
+
# 인자/옵션 오류(UsageError 포함)도 json 모드에서는 envelope로.
|
|
94
|
+
# table 모드는 Click 기본 출력 그대로 (re-raise).
|
|
95
|
+
if self._json_mode(ctx):
|
|
96
|
+
envelope.emit(error=envelope.error_body(
|
|
97
|
+
e.format_message(), code="INVALID_INPUT", retryable=False,
|
|
98
|
+
))
|
|
99
|
+
ctx.exit(e.exit_code)
|
|
100
|
+
raise
|
|
58
101
|
|
|
59
102
|
|
|
60
103
|
@click.group(cls=KiwoomGroup)
|
|
@@ -228,6 +271,17 @@ def config_profiles():
|
|
|
228
271
|
cfg = config.load_config()
|
|
229
272
|
profiles = cfg.get("profiles", {})
|
|
230
273
|
default = config.get_default_profile()
|
|
274
|
+
if _get_format() == "json":
|
|
275
|
+
envelope.emit(data=[
|
|
276
|
+
{
|
|
277
|
+
"name": name,
|
|
278
|
+
"domain": settings.get("domain", "mock"),
|
|
279
|
+
"account": settings.get("account", ""),
|
|
280
|
+
"default": name == default,
|
|
281
|
+
}
|
|
282
|
+
for name, settings in profiles.items()
|
|
283
|
+
])
|
|
284
|
+
return
|
|
231
285
|
if not profiles:
|
|
232
286
|
human("[yellow]등록된 프로필이 없습니다.[/]")
|
|
233
287
|
return
|
|
@@ -248,27 +302,43 @@ def auth_cmd():
|
|
|
248
302
|
pass
|
|
249
303
|
|
|
250
304
|
|
|
305
|
+
def _fail_not_configured():
|
|
306
|
+
msg = "설정이 필요합니다. 먼저 실행: kiwoom config setup"
|
|
307
|
+
if _get_format() == "json":
|
|
308
|
+
envelope.emit(error=envelope.error_body(msg, code="NOT_CONFIGURED", retryable=False))
|
|
309
|
+
else:
|
|
310
|
+
human(f"[red]{msg}[/]")
|
|
311
|
+
raise SystemExit(EXIT_INPUT)
|
|
312
|
+
|
|
313
|
+
|
|
251
314
|
@auth_cmd.command("login")
|
|
252
315
|
@click.pass_context
|
|
253
316
|
def auth_login(ctx):
|
|
254
317
|
"""접근토큰 발급."""
|
|
255
318
|
profile = config.resolve_profile(ctx.obj.get("profile") if ctx.obj else None)
|
|
256
319
|
if not config.is_configured(profile):
|
|
257
|
-
|
|
258
|
-
|
|
320
|
+
_fail_not_configured()
|
|
321
|
+
# 발급 실패(KiwoomAPIError)는 전역 핸들러가 envelope/exit 2로 처리
|
|
259
322
|
with KiwoomClient() as c:
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
323
|
+
token = c.issue_token()
|
|
324
|
+
storage = config.get_token_storage(profile)
|
|
325
|
+
if _get_format() == "json":
|
|
326
|
+
# env 모드에서는 사용자가 직접 관리해야 하므로 토큰 원문을 포함
|
|
327
|
+
envelope.emit(data={
|
|
328
|
+
"profile": profile,
|
|
329
|
+
"token_storage": storage,
|
|
330
|
+
"saved": storage != "env",
|
|
331
|
+
"token": token if storage == "env" else None,
|
|
332
|
+
})
|
|
333
|
+
return
|
|
334
|
+
human("[green]토큰 발급 완료![/]")
|
|
335
|
+
if storage == "env":
|
|
336
|
+
human(" 저장 위치: [bold]없음 (env 모드)[/] — 아래를 셸에서 실행하세요:")
|
|
337
|
+
human(f" export KIWOOM_TOKEN='{token}'")
|
|
338
|
+
else:
|
|
339
|
+
masked = token[:10] + "..." + token[-4:] if len(token) > 14 else token
|
|
340
|
+
human(f" 토큰: {masked}")
|
|
341
|
+
human(" 저장 위치: [bold]키체인[/]")
|
|
272
342
|
|
|
273
343
|
|
|
274
344
|
@auth_cmd.command("logout")
|
|
@@ -277,14 +347,14 @@ def auth_logout(ctx):
|
|
|
277
347
|
"""접근토큰 폐기."""
|
|
278
348
|
profile = config.resolve_profile(ctx.obj.get("profile") if ctx.obj else None)
|
|
279
349
|
if not config.is_configured(profile):
|
|
280
|
-
|
|
281
|
-
|
|
350
|
+
_fail_not_configured()
|
|
351
|
+
# 폐기 실패(KiwoomAPIError)는 전역 핸들러가 envelope/exit 2로 처리
|
|
282
352
|
with KiwoomClient() as c:
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
353
|
+
c.revoke_token()
|
|
354
|
+
if _get_format() == "json":
|
|
355
|
+
envelope.emit(data={"profile": profile, "revoked": True})
|
|
356
|
+
else:
|
|
357
|
+
human("[green]토큰 폐기 완료.[/]")
|
|
288
358
|
if os.environ.get("KIWOOM_TOKEN"):
|
|
289
359
|
human("[yellow]KIWOOM_TOKEN 환경변수가 설정되어 있어 이 셸에서는 해당 토큰이 계속 사용됩니다. unset KIWOOM_TOKEN 으로 제거하세요.[/]")
|
|
290
360
|
|
|
@@ -330,7 +400,8 @@ def auth_status(ctx):
|
|
|
330
400
|
@click.argument("api_id")
|
|
331
401
|
@click.argument("body", default="{}")
|
|
332
402
|
@click.option("--raw", is_flag=True, help="JSON 원본 출력")
|
|
333
|
-
|
|
403
|
+
@click.option("--next-key", "next_key", default="", help="연속조회 커서 (이전 응답의 meta.cont.next_key)")
|
|
404
|
+
def raw_api(api_id: str, body: str, raw: bool, next_key: str):
|
|
334
405
|
"""Raw API 호출. (예: kiwoom api ka10001 '{"stk_cd":"005930"}')"""
|
|
335
406
|
try:
|
|
336
407
|
body_dict = json.loads(body)
|
|
@@ -338,9 +409,17 @@ def raw_api(api_id: str, body: str, raw: bool):
|
|
|
338
409
|
raise click.ClickException(f"Invalid JSON body: {e}")
|
|
339
410
|
|
|
340
411
|
with KiwoomClient() as c:
|
|
341
|
-
data, headers = c.request(
|
|
412
|
+
data, headers = c.request(
|
|
413
|
+
api_id, body_dict,
|
|
414
|
+
cont_yn="Y" if next_key else "",
|
|
415
|
+
next_key=next_key,
|
|
416
|
+
)
|
|
342
417
|
if raw:
|
|
343
|
-
|
|
418
|
+
if _get_format() == "json":
|
|
419
|
+
# --raw는 필드 제거만 생략: stdout은 여전히 단일 envelope 문서
|
|
420
|
+
envelope.emit(data=data)
|
|
421
|
+
else:
|
|
422
|
+
console.print_json(json.dumps(data, ensure_ascii=False, indent=2))
|
|
344
423
|
else:
|
|
345
424
|
from .api_spec import get_description
|
|
346
425
|
title = get_description(api_id)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kiwoom-cli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: User-friendly CLI for the Kiwoom Securities REST API (키움증권)
|
|
5
5
|
License: LicenseRef-kiwoom-cli-Source-Available-1.0
|
|
6
6
|
Project-URL: Homepage, https://github.com/gejyn14/kiwoom-cli
|
|
@@ -512,13 +512,61 @@ kiwoom --no-color stock info 005930 # 색상 없이 (파일 저장용)
|
|
|
512
512
|
```
|
|
513
513
|
|
|
514
514
|
```bash
|
|
515
|
-
# jq로 필터링
|
|
516
|
-
kiwoom -f json market rank volume | jq '.[].stk_nm'
|
|
515
|
+
# jq로 필터링 (본문은 .data 아래)
|
|
516
|
+
kiwoom -f json market rank volume | jq '.data[].stk_nm'
|
|
517
517
|
|
|
518
518
|
# CSV를 파일로 저장
|
|
519
519
|
kiwoom -f csv stock daily 005930 > samsung_daily.csv
|
|
520
520
|
```
|
|
521
521
|
|
|
522
|
+
### JSON 응답 envelope (v1)
|
|
523
|
+
|
|
524
|
+
`-f json`의 모든 응답은 성공/실패 모두 하나의 안정적인 envelope로 감쌉니다. table/csv 모드는 그대로입니다.
|
|
525
|
+
|
|
526
|
+
```json
|
|
527
|
+
{
|
|
528
|
+
"ok": true,
|
|
529
|
+
"schema": "v1",
|
|
530
|
+
"data": { "stk_nm": "삼성전자", "cur_prc": "+70000" },
|
|
531
|
+
"meta": { "profile": "default", "env": "prod", "cont": { "next_key": "..." } },
|
|
532
|
+
"error": null
|
|
533
|
+
}
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
- `data` — 기존 응답 본문 그대로 (`return_code`/`return_msg`만 제거)
|
|
537
|
+
- `meta.profile` / `meta.env` — 해석된 프로필과 도메인(`prod`/`mock`)
|
|
538
|
+
- `meta.cont` — 연속조회 커서. 값이 있으면 다음 페이지가 존재: `kiwoom api <api_id> <body> --next-key <meta.cont.next_key>`
|
|
539
|
+
- 실패 시 `ok: false`, `data: null`이고 `error`에 안정적인 코드가 담깁니다:
|
|
540
|
+
|
|
541
|
+
```json
|
|
542
|
+
{ "code": "TOKEN_EXPIRED", "retryable": false, "message": "Token이 유효하지 않습니다", "upstream_code": 8005 }
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
`error.code`는 키움 오류코드/HTTP 상태를 분류한 stable enum입니다 — 에이전트는 메시지 문자열 대신 이 코드로 분기하세요:
|
|
546
|
+
|
|
547
|
+
| code | retryable | 의미 (upstream) |
|
|
548
|
+
| --- | :---: | --- |
|
|
549
|
+
| `INVALID_INPUT` | X | 입력 값/필수 파라미터 오류 (2, 1511, 1512, 1517) |
|
|
550
|
+
| `INVALID_API` | X | 잘못된 API ID (1501, 1504, 1505) |
|
|
551
|
+
| `NOT_FOUND` | X | 시장/종목 정보 없음 (1901, 1902) |
|
|
552
|
+
| `AUTH_REQUIRED` | X | 인증 헤더/토큰 누락 (1513~1516, 토큰 미보유 시 로컬 감지) |
|
|
553
|
+
| `TOKEN_EXPIRED` | X | 토큰 무효·만료 (8005, HTTP 401) |
|
|
554
|
+
| `TOKEN_ISSUE_FAILED` | X | 토큰 발급 실패 (8003, 8006, 8009, 8011, 8012) |
|
|
555
|
+
| `TOKEN_REVOKE_FAILED` | X | 토큰 폐기 실패 (8015, 8016) |
|
|
556
|
+
| `INVALID_CREDENTIALS` | X | appkey/secretkey 검증 실패 (8001, 8002, 8020) |
|
|
557
|
+
| `IP_MISMATCH` | X | 발급 IP와 요청 IP 불일치 (8010) |
|
|
558
|
+
| `ENV_MISMATCH` | X | 실전/모의 구분 불일치 (8030, 8031) |
|
|
559
|
+
| `DEVICE_AUTH_FAILED` | X | 단말기 인증 실패 (8040, 8050, 8103) |
|
|
560
|
+
| `RATE_LIMITED` | 1700·429는 O | 요청 개수 초과 (1700, HTTP 429) / 재귀 호출 제한 (1687) |
|
|
561
|
+
| `NETWORK_ERROR` | O | API 서버 연결 실패 |
|
|
562
|
+
| `KEYCHAIN_UNAVAILABLE` | X | OS 키체인 접근 불가 (샌드박스/CI) |
|
|
563
|
+
| `NOT_CONFIGURED` | X | `config setup` 미실행 (CLI 로컬 감지) |
|
|
564
|
+
| `UPSTREAM_ERROR` | 5xx·1999는 O | 미분류 업스트림 오류 (기본값) |
|
|
565
|
+
|
|
566
|
+
CLI 수준의 인자/옵션 오류(잘못된 값, 인자 누락 등)도 json 모드에서는 `INVALID_INPUT` envelope로 출력됩니다 (`upstream_code: null`, exit 1). `kiwoom api --raw`는 json 모드에서도 envelope로 감싸되 `data`에 응답 원본을 그대로(`return_code` 포함) 담습니다. `auth login`은 json 모드에서 `{profile, token_storage, saved, token}`을 반환하며, `token` 원문은 env 모드에서만 포함됩니다.
|
|
567
|
+
|
|
568
|
+
exit code 계약은 그대로입니다 (0=성공, 1=입력오류, 2=API오류, 3=인증필요).
|
|
569
|
+
|
|
522
570
|
## Shell 자동완성
|
|
523
571
|
|
|
524
572
|
```bash
|
|
@@ -6,6 +6,7 @@ kiwoom_cli/api_spec.py
|
|
|
6
6
|
kiwoom_cli/auth.py
|
|
7
7
|
kiwoom_cli/client.py
|
|
8
8
|
kiwoom_cli/config.py
|
|
9
|
+
kiwoom_cli/envelope.py
|
|
9
10
|
kiwoom_cli/formatters.py
|
|
10
11
|
kiwoom_cli/main.py
|
|
11
12
|
kiwoom_cli/output.py
|
|
@@ -38,6 +39,7 @@ tests/test_auth_cli.py
|
|
|
38
39
|
tests/test_cli.py
|
|
39
40
|
tests/test_client.py
|
|
40
41
|
tests/test_credentials.py
|
|
42
|
+
tests/test_envelope.py
|
|
41
43
|
tests/test_formatters.py
|
|
42
44
|
tests/test_formatters_property.py
|
|
43
45
|
tests/test_json_purity.py
|