kiwoom-cli 2.2.1__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.1 → kiwoom_cli-2.3.0}/PKG-INFO +51 -3
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/README.md +50 -2
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/kiwoom_cli/__init__.py +1 -1
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/kiwoom_cli/client.py +10 -0
- kiwoom_cli-2.3.0/kiwoom_cli/envelope.py +132 -0
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/kiwoom_cli/formatters.py +3 -3
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/kiwoom_cli/main.py +105 -42
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/kiwoom_cli.egg-info/PKG-INFO +51 -3
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/kiwoom_cli.egg-info/SOURCES.txt +2 -0
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/tests/test_auth_cli.py +4 -4
- {kiwoom_cli-2.2.1 → 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.1 → kiwoom_cli-2.3.0}/tests/test_formatters.py +8 -8
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/tests/test_json_purity.py +5 -3
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/tests/test_us_account.py +6 -6
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/LICENSE +0 -0
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/kiwoom_cli/api_spec.py +0 -0
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/kiwoom_cli/auth.py +0 -0
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/kiwoom_cli/commands/__init__.py +0 -0
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/kiwoom_cli/commands/_constants.py +0 -0
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/kiwoom_cli/commands/account.py +0 -0
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/kiwoom_cli/commands/dashboard.py +0 -0
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/kiwoom_cli/commands/market.py +0 -0
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/kiwoom_cli/commands/order.py +0 -0
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/kiwoom_cli/commands/stock.py +0 -0
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/kiwoom_cli/commands/stream.py +0 -0
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/kiwoom_cli/commands/us/__init__.py +0 -0
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/kiwoom_cli/commands/us/_constants.py +0 -0
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/kiwoom_cli/commands/us/account_ops.py +0 -0
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/kiwoom_cli/commands/us/detect.py +0 -0
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/kiwoom_cli/commands/us/exchange.py +0 -0
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/kiwoom_cli/commands/us/order_ops.py +0 -0
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/kiwoom_cli/commands/us/stock_ops.py +0 -0
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/kiwoom_cli/commands/watch.py +0 -0
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/kiwoom_cli/config.py +0 -0
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/kiwoom_cli/output.py +0 -0
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/kiwoom_cli/streaming.py +0 -0
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/kiwoom_cli.egg-info/dependency_links.txt +0 -0
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/kiwoom_cli.egg-info/entry_points.txt +0 -0
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/kiwoom_cli.egg-info/requires.txt +0 -0
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/kiwoom_cli.egg-info/top_level.txt +0 -0
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/pyproject.toml +0 -0
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/setup.cfg +0 -0
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/tests/test_account.py +0 -0
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/tests/test_auth.py +0 -0
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/tests/test_client.py +0 -0
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/tests/test_credentials.py +0 -0
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/tests/test_formatters_property.py +0 -0
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/tests/test_market.py +0 -0
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/tests/test_migration.py +0 -0
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/tests/test_order.py +0 -0
- {kiwoom_cli-2.2.1 → kiwoom_cli-2.3.0}/tests/test_stock.py +0 -0
- {kiwoom_cli-2.2.1 → 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
|
|
@@ -104,6 +104,16 @@ class KiwoomClient:
|
|
|
104
104
|
"cont-yn": resp.headers.get("cont-yn", ""),
|
|
105
105
|
"next-key": resp.headers.get("next-key", ""),
|
|
106
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
|
+
|
|
107
117
|
return data, resp_headers
|
|
108
118
|
|
|
109
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:
|
|
@@ -9,7 +9,7 @@ import click
|
|
|
9
9
|
import httpx
|
|
10
10
|
import keyring
|
|
11
11
|
|
|
12
|
-
from . import __version__, auth, config
|
|
12
|
+
from . import __version__, auth, config, envelope
|
|
13
13
|
from .client import KiwoomClient, KiwoomAPIError, KiwoomAuthError
|
|
14
14
|
from .commands.account import account
|
|
15
15
|
from .commands.dashboard import dashboard
|
|
@@ -36,41 +36,68 @@ click.exceptions.UsageError.exit_code = EXIT_INPUT
|
|
|
36
36
|
class KiwoomGroup(click.Group):
|
|
37
37
|
"""Custom group that catches API/network errors globally."""
|
|
38
38
|
|
|
39
|
+
@staticmethod
|
|
40
|
+
def _json_mode(ctx) -> bool:
|
|
41
|
+
return (ctx.obj.get("format", "table") if ctx.obj else "table") == "json"
|
|
42
|
+
|
|
39
43
|
def invoke(self, ctx):
|
|
40
44
|
try:
|
|
41
45
|
return super().invoke(ctx)
|
|
42
46
|
except KiwoomAPIError as e:
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
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))
|
|
46
49
|
else:
|
|
47
50
|
console.print(f"[red]API 오류:[/] {e}")
|
|
48
51
|
ctx.exit(EXIT_API)
|
|
49
52
|
except KiwoomAuthError:
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
if self._json_mode(ctx):
|
|
54
|
+
envelope.emit(error=envelope.error_body(
|
|
55
|
+
"토큰이 없습니다. 'kiwoom auth login'으로 발급하세요.",
|
|
56
|
+
code="AUTH_REQUIRED", retryable=False,
|
|
57
|
+
))
|
|
54
58
|
else:
|
|
55
59
|
console.print("[red]인증 필요:[/] 토큰이 없습니다. [dim]kiwoom auth login[/]")
|
|
56
60
|
ctx.exit(EXIT_AUTH)
|
|
57
61
|
except keyring.errors.KeyringError:
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
+
)
|
|
63
73
|
ctx.exit(EXIT_INPUT)
|
|
64
74
|
except httpx.HTTPStatusError as e:
|
|
65
|
-
|
|
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:
|
|
66
79
|
console.print("[red]인증 오류:[/] 토큰이 만료되었습니다. [dim]kiwoom auth login[/]")
|
|
67
|
-
ctx.exit(EXIT_AUTH)
|
|
68
80
|
else:
|
|
69
|
-
console.print(f"[red]HTTP 오류:[/] {
|
|
70
|
-
|
|
81
|
+
console.print(f"[red]HTTP 오류:[/] {status}")
|
|
82
|
+
ctx.exit(EXIT_AUTH if status == 401 else EXIT_API)
|
|
71
83
|
except httpx.ConnectError:
|
|
72
|
-
|
|
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 서버에 연결할 수 없습니다. 도메인을 확인하세요.")
|
|
73
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
|
|
74
101
|
|
|
75
102
|
|
|
76
103
|
@click.group(cls=KiwoomGroup)
|
|
@@ -244,6 +271,17 @@ def config_profiles():
|
|
|
244
271
|
cfg = config.load_config()
|
|
245
272
|
profiles = cfg.get("profiles", {})
|
|
246
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
|
|
247
285
|
if not profiles:
|
|
248
286
|
human("[yellow]등록된 프로필이 없습니다.[/]")
|
|
249
287
|
return
|
|
@@ -264,27 +302,43 @@ def auth_cmd():
|
|
|
264
302
|
pass
|
|
265
303
|
|
|
266
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
|
+
|
|
267
314
|
@auth_cmd.command("login")
|
|
268
315
|
@click.pass_context
|
|
269
316
|
def auth_login(ctx):
|
|
270
317
|
"""접근토큰 발급."""
|
|
271
318
|
profile = config.resolve_profile(ctx.obj.get("profile") if ctx.obj else None)
|
|
272
319
|
if not config.is_configured(profile):
|
|
273
|
-
|
|
274
|
-
|
|
320
|
+
_fail_not_configured()
|
|
321
|
+
# 발급 실패(KiwoomAPIError)는 전역 핸들러가 envelope/exit 2로 처리
|
|
275
322
|
with KiwoomClient() as c:
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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]키체인[/]")
|
|
288
342
|
|
|
289
343
|
|
|
290
344
|
@auth_cmd.command("logout")
|
|
@@ -293,14 +347,14 @@ def auth_logout(ctx):
|
|
|
293
347
|
"""접근토큰 폐기."""
|
|
294
348
|
profile = config.resolve_profile(ctx.obj.get("profile") if ctx.obj else None)
|
|
295
349
|
if not config.is_configured(profile):
|
|
296
|
-
|
|
297
|
-
|
|
350
|
+
_fail_not_configured()
|
|
351
|
+
# 폐기 실패(KiwoomAPIError)는 전역 핸들러가 envelope/exit 2로 처리
|
|
298
352
|
with KiwoomClient() as c:
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
353
|
+
c.revoke_token()
|
|
354
|
+
if _get_format() == "json":
|
|
355
|
+
envelope.emit(data={"profile": profile, "revoked": True})
|
|
356
|
+
else:
|
|
357
|
+
human("[green]토큰 폐기 완료.[/]")
|
|
304
358
|
if os.environ.get("KIWOOM_TOKEN"):
|
|
305
359
|
human("[yellow]KIWOOM_TOKEN 환경변수가 설정되어 있어 이 셸에서는 해당 토큰이 계속 사용됩니다. unset KIWOOM_TOKEN 으로 제거하세요.[/]")
|
|
306
360
|
|
|
@@ -346,7 +400,8 @@ def auth_status(ctx):
|
|
|
346
400
|
@click.argument("api_id")
|
|
347
401
|
@click.argument("body", default="{}")
|
|
348
402
|
@click.option("--raw", is_flag=True, help="JSON 원본 출력")
|
|
349
|
-
|
|
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):
|
|
350
405
|
"""Raw API 호출. (예: kiwoom api ka10001 '{"stk_cd":"005930"}')"""
|
|
351
406
|
try:
|
|
352
407
|
body_dict = json.loads(body)
|
|
@@ -354,9 +409,17 @@ def raw_api(api_id: str, body: str, raw: bool):
|
|
|
354
409
|
raise click.ClickException(f"Invalid JSON body: {e}")
|
|
355
410
|
|
|
356
411
|
with KiwoomClient() as c:
|
|
357
|
-
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
|
+
)
|
|
358
417
|
if raw:
|
|
359
|
-
|
|
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))
|
|
360
423
|
else:
|
|
361
424
|
from .api_spec import get_description
|
|
362
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
|
|
@@ -206,8 +206,8 @@ def test_auth_status_shows_env_storage_mode(mem_keyring):
|
|
|
206
206
|
result = runner.invoke(cli, ["-f", "json", "auth", "status"])
|
|
207
207
|
assert result.exit_code == 0
|
|
208
208
|
doc = _json.loads(result.stdout)
|
|
209
|
-
assert doc["token_storage"] == "env"
|
|
210
|
-
assert doc["has_token"] is False
|
|
209
|
+
assert doc["data"]["token_storage"] == "env"
|
|
210
|
+
assert doc["data"]["has_token"] is False
|
|
211
211
|
|
|
212
212
|
result = runner.invoke(cli, ["auth", "status"])
|
|
213
213
|
assert result.exit_code == 0
|
|
@@ -228,8 +228,8 @@ def test_auth_status_reflects_env_token_with_broken_keyring(monkeypatch):
|
|
|
228
228
|
result = runner.invoke(cli, ["-f", "json", "auth", "status"])
|
|
229
229
|
assert result.exit_code == 0
|
|
230
230
|
doc = _json.loads(result.stdout)
|
|
231
|
-
assert doc["has_token"] is True
|
|
232
|
-
assert doc["token_source"] == "env"
|
|
231
|
+
assert doc["data"]["has_token"] is True
|
|
232
|
+
assert doc["data"]["token_source"] == "env"
|
|
233
233
|
|
|
234
234
|
result = runner.invoke(cli, ["auth", "status"])
|
|
235
235
|
assert result.exit_code == 0
|