krx-openapi 0.2.0__tar.gz → 0.2.2__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.
- {krx_openapi-0.2.0 → krx_openapi-0.2.2}/PKG-INFO +1 -1
- {krx_openapi-0.2.0 → krx_openapi-0.2.2}/pyproject.toml +1 -1
- {krx_openapi-0.2.0 → krx_openapi-0.2.2}/src/krx_openapi/errors.py +3 -1
- {krx_openapi-0.2.0 → krx_openapi-0.2.2}/src/krx_openapi/session.py +62 -21
- {krx_openapi-0.2.0 → krx_openapi-0.2.2}/tests/test_session.py +79 -15
- {krx_openapi-0.2.0 → krx_openapi-0.2.2}/.agents/plugins/marketplace.json +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.2}/.claude-plugin/marketplace.json +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.2}/.github/workflows/check.yml +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.2}/.github/workflows/publish.yml +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.2}/.github/workflows/reusable-check.yml +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.2}/.gitignore +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.2}/LICENSE +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.2}/Makefile +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.2}/README.en.md +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.2}/README.md +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.2}/plugins/krx/.claude-plugin/plugin.json +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.2}/plugins/krx/.codex-plugin/plugin.json +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.2}/plugins/krx/skills/fetch/SKILL.md +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.2}/plugins/krx/skills/fields/SKILL.md +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.2}/plugins/krx/skills/list/SKILL.md +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.2}/src/krx_openapi/__init__.py +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.2}/src/krx_openapi/__main__.py +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.2}/src/krx_openapi/_config.py +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.2}/src/krx_openapi/_endpoint.py +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.2}/src/krx_openapi/catalog.py +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.2}/src/krx_openapi/cli.py +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.2}/src/krx_openapi/client.py +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.2}/src/krx_openapi/py.typed +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.2}/src/krx_openapi/types.py +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.2}/tests/__init__.py +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.2}/tests/conftest.py +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.2}/tests/test_catalog.py +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.2}/tests/test_cli.py +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.2}/tests/test_client.py +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.2}/tests/test_config.py +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.2}/tests/test_endpoints.py +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.2}/tests/test_errors.py +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.2}/tests/test_live.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: krx-openapi
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Read the Korea Exchange (KRX) Open API: indices, stocks, ETP, bonds, derivatives, commodities, ESG.
|
|
5
5
|
Project-URL: Homepage, https://github.com/seokhoonj/krx-openapi
|
|
6
6
|
Project-URL: Repository, https://github.com/seokhoonj/krx-openapi
|
|
@@ -7,7 +7,7 @@ build-backend = "hatchling.build"
|
|
|
7
7
|
|
|
8
8
|
[project]
|
|
9
9
|
name = "krx-openapi"
|
|
10
|
-
version = "0.2.
|
|
10
|
+
version = "0.2.2"
|
|
11
11
|
description = "Read the Korea Exchange (KRX) Open API: indices, stocks, ETP, bonds, derivatives, commodities, ESG."
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
requires-python = ">=3.11"
|
|
@@ -36,7 +36,9 @@ class KRXNetworkError(KRXError):
|
|
|
36
36
|
|
|
37
37
|
A timeout, DNS failure, connection reset, an interrupted read, a non-success HTTP
|
|
38
38
|
status, or a 200 whose body is not JSON (a proxy/maintenance page). The underlying
|
|
39
|
-
exception is chained
|
|
39
|
+
transport/decode exception is deliberately NOT chained: it can hold the request URL
|
|
40
|
+
or the response body, so surfacing it on ``__cause__``/``__context__`` would expose
|
|
41
|
+
them to a logger that walks the chain.
|
|
40
42
|
"""
|
|
41
43
|
|
|
42
44
|
|
|
@@ -21,7 +21,7 @@ from typing import Any
|
|
|
21
21
|
|
|
22
22
|
from ._config import resolve_api_key
|
|
23
23
|
from ._endpoint import SAMPLE_DATE, SAMPLE_KEY, KRXEndpoint
|
|
24
|
-
from .errors import KRXNetworkError, KRXRateLimitError, _error_for
|
|
24
|
+
from .errors import KRXError, KRXNetworkError, KRXRateLimitError, _error_for
|
|
25
25
|
from .types import Row
|
|
26
26
|
|
|
27
27
|
_AUTH_HEADER = "AUTH_KEY"
|
|
@@ -99,51 +99,92 @@ class KRXSession:
|
|
|
99
99
|
request = urllib.request.Request(
|
|
100
100
|
full_url, headers={_AUTH_HEADER: api_key.strip(), "User-Agent": _USER_AGENT}
|
|
101
101
|
)
|
|
102
|
+
# The key rides in the AUTH_KEY header, not the URL, so `url` is safe to name.
|
|
103
|
+
# Still, every failure is BUILT inside the except and RAISED after it
|
|
104
|
+
# (`from None`), so no transport exception is chained -- a reflecting server that
|
|
105
|
+
# echoed the request header into a response body could otherwise reach the key
|
|
106
|
+
# through, e.g., an IncompleteRead's partial body on the cause chain.
|
|
107
|
+
failure: KRXError
|
|
102
108
|
try:
|
|
103
109
|
with _OPENER.open(request, timeout=self.timeout) as response:
|
|
104
110
|
raw = response.read()
|
|
105
111
|
except urllib.error.HTTPError as err:
|
|
106
112
|
with err: # an HTTPError is an unclosed response; release its socket
|
|
107
113
|
if err.code == _RATE_LIMIT_STATUS:
|
|
108
|
-
|
|
114
|
+
failure = KRXRateLimitError(
|
|
109
115
|
"KRX daily call quota exceeded (10,000/key/day); resets at "
|
|
110
|
-
"midnight")
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
+
"midnight")
|
|
117
|
+
else:
|
|
118
|
+
hint = ""
|
|
119
|
+
if err.code == _FORBIDDEN_STATUS:
|
|
120
|
+
hint = (" -- check the key, that the URL is https and /svc/apis "
|
|
121
|
+
"(not /svc/sample/apis with a real key)")
|
|
122
|
+
failure = KRXNetworkError(f"HTTP {err.code} from KRX for {url}{hint}")
|
|
116
123
|
except urllib.error.URLError as err:
|
|
117
|
-
|
|
124
|
+
# A transport reason is external text; keep only its type, then detach below.
|
|
125
|
+
failure = KRXNetworkError(
|
|
126
|
+
f"request to KRX failed for {url}: {type(err.reason).__name__}")
|
|
118
127
|
except (http.client.HTTPException, OSError) as err:
|
|
119
128
|
# A failure during response.read() (IncompleteRead, a socket timeout or
|
|
120
|
-
# reset) is not an HTTPError/URLError; surface it through KRXError too.
|
|
121
|
-
|
|
122
|
-
|
|
129
|
+
# reset) is not an HTTPError/URLError; surface it through KRXError too. Name
|
|
130
|
+
# only the exception type -- str(err) can hold partially-read body bytes.
|
|
131
|
+
failure = KRXNetworkError(
|
|
132
|
+
f"KRX response read failed for {url}: {type(err).__name__}")
|
|
133
|
+
else:
|
|
134
|
+
return _rows_from_body(raw, url, api_key)
|
|
135
|
+
raise failure from None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _redact_key(text: str, api_key: str) -> str:
|
|
139
|
+
"""Blank the API key out of server-authored text (a vendor message, or a body a
|
|
140
|
+
reflecting server echoed the ``AUTH_KEY`` header into).
|
|
141
|
+
|
|
142
|
+
krx carries the key in the request header, not the URL, so this is defense-in-depth:
|
|
143
|
+
a server that reflected the header into a response field would otherwise surface it in
|
|
144
|
+
an error message. Blank the raw and url-encoded forms of the value actually sent
|
|
145
|
+
(``api_key.strip()``). (An empty key would splice ``<key>`` between every character.)
|
|
146
|
+
"""
|
|
147
|
+
key = api_key.strip()
|
|
148
|
+
if not key:
|
|
149
|
+
return text
|
|
150
|
+
return text.replace(key, "<key>").replace(urllib.parse.quote_plus(key), "<key>")
|
|
123
151
|
|
|
124
152
|
|
|
125
|
-
def _rows_from_body(raw: bytes, url: str) -> list[Row]:
|
|
153
|
+
def _rows_from_body(raw: bytes, url: str, api_key: str) -> list[Row]:
|
|
126
154
|
"""Apply the KRX status contract to a raw response body.
|
|
127
155
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
156
|
+
Parses JSON, raises on a non-empty ``respCode`` or a non-JSON body, and returns the
|
|
157
|
+
single ``OutBlock_*`` array (``[]`` when the vendor sends an empty one for a no-data
|
|
158
|
+
date). ``api_key`` is used only to redact it from an error message, in case a server
|
|
159
|
+
echoes the request header into the body.
|
|
131
160
|
"""
|
|
161
|
+
failure: KRXError | None = None
|
|
132
162
|
try:
|
|
133
163
|
# Decode as UTF-8 explicitly: json.loads(bytes) auto-detects UTF-16/32, which
|
|
134
164
|
# would let a non-UTF-8 body slip through the "must be UTF-8 JSON" contract.
|
|
135
165
|
body: Any = json.loads(raw.decode("utf-8"))
|
|
136
|
-
except (json.JSONDecodeError, UnicodeDecodeError, RecursionError)
|
|
166
|
+
except (json.JSONDecodeError, UnicodeDecodeError, RecursionError):
|
|
137
167
|
# A 200 whose body is not JSON / not UTF-8 (a proxy or maintenance HTML page), or
|
|
138
168
|
# is nested deep enough to blow the parser's recursion limit, must surface through
|
|
139
|
-
# KRXError
|
|
140
|
-
|
|
169
|
+
# KRXError. Built here but raised OUTSIDE the except (`from None` below): the decode
|
|
170
|
+
# error's .doc/.object is the whole body -- which a reflecting server could make
|
|
171
|
+
# echo the AUTH_KEY header -- so it must not ride __cause__/__context__.
|
|
172
|
+
failure = KRXNetworkError(f"non-JSON response from KRX for {url}")
|
|
173
|
+
if failure is not None:
|
|
174
|
+
raise failure from None
|
|
175
|
+
|
|
141
176
|
if not isinstance(body, dict):
|
|
142
|
-
|
|
177
|
+
# Name only the shape -- body is server-authored and could echo the header key.
|
|
178
|
+
raise KRXNetworkError(
|
|
179
|
+
f"unexpected KRX response for {url}: expected a JSON object, "
|
|
180
|
+
f"got {type(body).__name__}")
|
|
143
181
|
|
|
144
182
|
code = body.get("respCode")
|
|
145
183
|
if code: # present only on failure
|
|
146
|
-
|
|
184
|
+
# respCode/respMsg are server-authored; redact in case they echo the header key.
|
|
185
|
+
raise _error_for(
|
|
186
|
+
_redact_key(str(code), api_key),
|
|
187
|
+
_redact_key(str(body.get("respMsg", "")), api_key))
|
|
147
188
|
|
|
148
189
|
blocks = [
|
|
149
190
|
value for key, value in body.items()
|
|
@@ -31,69 +31,69 @@ def _body(payload):
|
|
|
31
31
|
def test_extracts_the_outblock_array():
|
|
32
32
|
rows = _rows_from_body(
|
|
33
33
|
_body({"OutBlock_1": [{"BAS_DD": "20200414"}, {"BAS_DD": "20200414"}]}),
|
|
34
|
-
"http://x")
|
|
34
|
+
"http://x", "")
|
|
35
35
|
assert rows == [{"BAS_DD": "20200414"}, {"BAS_DD": "20200414"}]
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
def test_empty_outblock_is_empty_list():
|
|
39
39
|
# a no-data date: KRX still returns the OutBlock, just empty
|
|
40
|
-
assert _rows_from_body(_body({"OutBlock_1": []}), "http://x") == []
|
|
40
|
+
assert _rows_from_body(_body({"OutBlock_1": []}), "http://x", "") == []
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
def test_no_outblock_array_raises():
|
|
44
44
|
with pytest.raises(KRXNetworkError):
|
|
45
|
-
_rows_from_body(_body({"other": "1"}), "http://x")
|
|
45
|
+
_rows_from_body(_body({"other": "1"}), "http://x", "")
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
def test_multiple_outblock_arrays_raise():
|
|
49
49
|
with pytest.raises(KRXNetworkError):
|
|
50
|
-
_rows_from_body(_body({"OutBlock_1": [], "OutBlock_2": []}), "http://x")
|
|
50
|
+
_rows_from_body(_body({"OutBlock_1": [], "OutBlock_2": []}), "http://x", "")
|
|
51
51
|
|
|
52
52
|
|
|
53
53
|
def test_outblock_prefix_requires_the_underscore():
|
|
54
54
|
# The block key must be OutBlock_<n>. A bare "OutBlock" is not a data array, so a
|
|
55
55
|
# body carrying only that is no recognized block -> raises.
|
|
56
56
|
with pytest.raises(KRXNetworkError):
|
|
57
|
-
_rows_from_body(_body({"OutBlock": []}), "http://x")
|
|
57
|
+
_rows_from_body(_body({"OutBlock": []}), "http://x", "")
|
|
58
58
|
# A near-prefix like "OutBlockMeta" must be ignored, not mistaken for a second data
|
|
59
59
|
# block: with the real OutBlock_1 present, exactly one block is found and returned.
|
|
60
60
|
# (Under the looser startswith("OutBlock") this would be two blocks and would raise,
|
|
61
61
|
# so this pins the underscore.)
|
|
62
|
-
rows = _rows_from_body(_body({"OutBlockMeta": [], "OutBlock_1": [{"a": "1"}]}), "http://x")
|
|
62
|
+
rows = _rows_from_body(_body({"OutBlockMeta": [], "OutBlock_1": [{"a": "1"}]}), "http://x", "")
|
|
63
63
|
assert rows == [{"a": "1"}]
|
|
64
64
|
|
|
65
65
|
|
|
66
66
|
def test_respcode_401_raises_auth_error():
|
|
67
67
|
with pytest.raises(KRXAuthError):
|
|
68
|
-
_rows_from_body(_body({"respCode": "401", "respMsg": "not applied"}), "http://x")
|
|
68
|
+
_rows_from_body(_body({"respCode": "401", "respMsg": "not applied"}), "http://x", "")
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
def test_respcode_other_raises_response_error():
|
|
72
72
|
with pytest.raises(KRXResponseError):
|
|
73
|
-
_rows_from_body(_body({"respCode": "500", "respMsg": "boom"}), "http://x")
|
|
73
|
+
_rows_from_body(_body({"respCode": "500", "respMsg": "boom"}), "http://x", "")
|
|
74
74
|
|
|
75
75
|
|
|
76
76
|
def test_non_json_raises_network_error():
|
|
77
77
|
with pytest.raises(KRXNetworkError):
|
|
78
|
-
_rows_from_body(b"<html>maintenance</html>", "http://x")
|
|
78
|
+
_rows_from_body(b"<html>maintenance</html>", "http://x", "")
|
|
79
79
|
|
|
80
80
|
|
|
81
81
|
def test_invalid_utf8_raises_network_error():
|
|
82
82
|
# json.loads on non-UTF-8 bytes raises UnicodeDecodeError, not JSONDecodeError
|
|
83
83
|
with pytest.raises(KRXNetworkError):
|
|
84
|
-
_rows_from_body(b"\xff\xfe not utf-8", "http://x")
|
|
84
|
+
_rows_from_body(b"\xff\xfe not utf-8", "http://x", "")
|
|
85
85
|
|
|
86
86
|
|
|
87
87
|
def test_valid_utf16_json_raises_network_error():
|
|
88
88
|
# Well-formed JSON, but UTF-16: json.loads(bytes) would auto-detect and accept it;
|
|
89
89
|
# the contract is UTF-8 only, so decoding explicitly must reject it.
|
|
90
90
|
with pytest.raises(KRXNetworkError):
|
|
91
|
-
_rows_from_body('{"OutBlock_1": []}'.encode("utf-16"), "http://x")
|
|
91
|
+
_rows_from_body('{"OutBlock_1": []}'.encode("utf-16"), "http://x", "")
|
|
92
92
|
|
|
93
93
|
|
|
94
94
|
def test_non_object_json_raises_network_error():
|
|
95
95
|
with pytest.raises(KRXNetworkError):
|
|
96
|
-
_rows_from_body(b"[1, 2, 3]", "http://x")
|
|
96
|
+
_rows_from_body(b"[1, 2, 3]", "http://x", "")
|
|
97
97
|
|
|
98
98
|
|
|
99
99
|
def test_recursion_error_becomes_network_error(monkeypatch):
|
|
@@ -106,12 +106,12 @@ def test_recursion_error_becomes_network_error(monkeypatch):
|
|
|
106
106
|
|
|
107
107
|
monkeypatch.setattr("krx_openapi.session.json.loads", boom)
|
|
108
108
|
with pytest.raises(KRXNetworkError):
|
|
109
|
-
_rows_from_body(b'{"OutBlock_1": []}', "http://x")
|
|
109
|
+
_rows_from_body(b'{"OutBlock_1": []}', "http://x", "")
|
|
110
110
|
|
|
111
111
|
|
|
112
112
|
def test_non_dict_row_raises():
|
|
113
113
|
with pytest.raises(KRXNetworkError):
|
|
114
|
-
_rows_from_body(_body({"OutBlock_1": [{"a": "1"}, "junk"]}), "http://x")
|
|
114
|
+
_rows_from_body(_body({"OutBlock_1": [{"a": "1"}, "junk"]}), "http://x", "")
|
|
115
115
|
|
|
116
116
|
|
|
117
117
|
# --- _get (offline opener fake) ---------------------------------------------
|
|
@@ -199,6 +199,68 @@ def test_get_read_failure_raises_network(monkeypatch):
|
|
|
199
199
|
KRXSession(api_key="k").fetch(ENDPOINTS["kospi_dd_trd"], basDd="20200414")
|
|
200
200
|
|
|
201
201
|
|
|
202
|
+
def test_read_failure_echoing_the_key_never_leaks_it(monkeypatch):
|
|
203
|
+
# A reflecting server could echo the AUTH_KEY header into the partial body of an
|
|
204
|
+
# interrupted read; the read-phase error must not surface it or chain the raw
|
|
205
|
+
# exception (whose partial body would carry it on the cause chain).
|
|
206
|
+
key = "SECRETKEY123"
|
|
207
|
+
|
|
208
|
+
class _EchoingRead(_FakeResponse):
|
|
209
|
+
def read(self) -> bytes:
|
|
210
|
+
raise http.client.IncompleteRead(f"partial Auth_key={key}".encode())
|
|
211
|
+
|
|
212
|
+
_patch_opener(monkeypatch, lambda request, timeout=None: _EchoingRead(b""))
|
|
213
|
+
with pytest.raises(KRXNetworkError) as exc:
|
|
214
|
+
KRXSession(api_key=key).fetch(ENDPOINTS["kospi_dd_trd"], basDd="20200414")
|
|
215
|
+
assert key not in str(exc.value)
|
|
216
|
+
assert exc.value.__context__ is None and exc.value.__cause__ is None
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# --- parse-path secret safety (the key must not ride the decode-error chain) --
|
|
220
|
+
|
|
221
|
+
def _assert_no_key(exc, key):
|
|
222
|
+
seen, pending = set(), [exc]
|
|
223
|
+
while pending:
|
|
224
|
+
current = pending.pop()
|
|
225
|
+
if current is None or id(current) in seen:
|
|
226
|
+
continue
|
|
227
|
+
seen.add(id(current))
|
|
228
|
+
blob = str(current) + repr(current) + repr(current.args)
|
|
229
|
+
assert key not in blob
|
|
230
|
+
pending.extend([current.__cause__, current.__context__])
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def test_non_utf8_body_echoing_the_key_never_leaks_it():
|
|
234
|
+
# A reflecting server could echo the AUTH_KEY header into a non-UTF-8 200 body; the
|
|
235
|
+
# decode error (whose .object is the whole body) must not ride the raised error's
|
|
236
|
+
# chain. This is the sibling of the read-phase test, for the body-parse path.
|
|
237
|
+
key = "SECRETKEY123"
|
|
238
|
+
body = ("bad " + key).encode() + b"\xff" # invalid UTF-8 tail, echoes the key
|
|
239
|
+
with pytest.raises(KRXNetworkError) as exc:
|
|
240
|
+
_rows_from_body(body, "http://x", key)
|
|
241
|
+
assert exc.value.__cause__ is None and exc.value.__context__ is None
|
|
242
|
+
_assert_no_key(exc.value, key)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def test_non_dict_body_echoing_the_key_never_leaks_it():
|
|
246
|
+
# A non-dict JSON body is server-authored; the "unexpected response" error must name
|
|
247
|
+
# only the shape, never the body content (which could echo the header key).
|
|
248
|
+
key = "SECRETKEY123"
|
|
249
|
+
body = _body([f"{key} reflected"]) # a JSON list echoing the key
|
|
250
|
+
with pytest.raises(KRXNetworkError) as exc:
|
|
251
|
+
_rows_from_body(body, "http://x", key)
|
|
252
|
+
_assert_no_key(exc.value, key)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def test_vendor_error_message_echoing_the_key_never_leaks_it():
|
|
256
|
+
# respCode/respMsg are server-authored; a message echoing the header key is redacted.
|
|
257
|
+
key = "SECRETKEY123"
|
|
258
|
+
body = _body({"respCode": "99", "respMsg": f"rejected {key}"})
|
|
259
|
+
with pytest.raises(KRXResponseError) as exc:
|
|
260
|
+
_rows_from_body(body, "http://x", key)
|
|
261
|
+
_assert_no_key(exc.value, key)
|
|
262
|
+
|
|
263
|
+
|
|
202
264
|
# --- redirects must never carry the key -------------------------------------
|
|
203
265
|
|
|
204
266
|
def test_no_redirect_handler_refuses_and_never_follows():
|
|
@@ -254,4 +316,6 @@ def test_error_message_never_shows_the_key(monkeypatch):
|
|
|
254
316
|
with pytest.raises(KRXNetworkError) as exc:
|
|
255
317
|
KRXSession(api_key="SECRETKEY123").fetch(ENDPOINTS["kospi_dd_trd"], basDd="20200414")
|
|
256
318
|
assert "SECRETKEY123" not in str(exc.value)
|
|
257
|
-
|
|
319
|
+
# The HTTPError branch also detaches: the key-bearing transport error must not ride
|
|
320
|
+
# the chain (str(__cause__) alone would pass even if it were chained, so assert None).
|
|
321
|
+
assert exc.value.__cause__ is None and exc.value.__context__ 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
|
|
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
|