krx-openapi 0.2.0__tar.gz → 0.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.
- {krx_openapi-0.2.0 → krx_openapi-0.2.1}/PKG-INFO +1 -1
- {krx_openapi-0.2.0 → krx_openapi-0.2.1}/pyproject.toml +1 -1
- {krx_openapi-0.2.0 → krx_openapi-0.2.1}/src/krx_openapi/session.py +25 -12
- {krx_openapi-0.2.0 → krx_openapi-0.2.1}/tests/test_session.py +17 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.1}/.agents/plugins/marketplace.json +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.1}/.claude-plugin/marketplace.json +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.1}/.github/workflows/check.yml +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.1}/.github/workflows/publish.yml +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.1}/.github/workflows/reusable-check.yml +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.1}/.gitignore +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.1}/LICENSE +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.1}/Makefile +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.1}/README.en.md +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.1}/README.md +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.1}/plugins/krx/.claude-plugin/plugin.json +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.1}/plugins/krx/.codex-plugin/plugin.json +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.1}/plugins/krx/skills/fetch/SKILL.md +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.1}/plugins/krx/skills/fields/SKILL.md +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.1}/plugins/krx/skills/list/SKILL.md +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.1}/src/krx_openapi/__init__.py +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.1}/src/krx_openapi/__main__.py +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.1}/src/krx_openapi/_config.py +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.1}/src/krx_openapi/_endpoint.py +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.1}/src/krx_openapi/catalog.py +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.1}/src/krx_openapi/cli.py +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.1}/src/krx_openapi/client.py +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.1}/src/krx_openapi/errors.py +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.1}/src/krx_openapi/py.typed +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.1}/src/krx_openapi/types.py +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.1}/tests/__init__.py +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.1}/tests/conftest.py +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.1}/tests/test_catalog.py +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.1}/tests/test_cli.py +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.1}/tests/test_client.py +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.1}/tests/test_config.py +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.1}/tests/test_endpoints.py +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.1}/tests/test_errors.py +0 -0
- {krx_openapi-0.2.0 → krx_openapi-0.2.1}/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.1
|
|
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.1"
|
|
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"
|
|
@@ -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,27 +99,40 @@ 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)
|
|
135
|
+
raise failure from None
|
|
123
136
|
|
|
124
137
|
|
|
125
138
|
def _rows_from_body(raw: bytes, url: str) -> list[Row]:
|
|
@@ -199,6 +199,23 @@ 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
|
+
|
|
202
219
|
# --- redirects must never carry the key -------------------------------------
|
|
203
220
|
|
|
204
221
|
def test_no_redirect_handler_refuses_and_never_follows():
|
|
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
|