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.
Files changed (38) hide show
  1. {krx_openapi-0.2.0 → krx_openapi-0.2.1}/PKG-INFO +1 -1
  2. {krx_openapi-0.2.0 → krx_openapi-0.2.1}/pyproject.toml +1 -1
  3. {krx_openapi-0.2.0 → krx_openapi-0.2.1}/src/krx_openapi/session.py +25 -12
  4. {krx_openapi-0.2.0 → krx_openapi-0.2.1}/tests/test_session.py +17 -0
  5. {krx_openapi-0.2.0 → krx_openapi-0.2.1}/.agents/plugins/marketplace.json +0 -0
  6. {krx_openapi-0.2.0 → krx_openapi-0.2.1}/.claude-plugin/marketplace.json +0 -0
  7. {krx_openapi-0.2.0 → krx_openapi-0.2.1}/.github/workflows/check.yml +0 -0
  8. {krx_openapi-0.2.0 → krx_openapi-0.2.1}/.github/workflows/publish.yml +0 -0
  9. {krx_openapi-0.2.0 → krx_openapi-0.2.1}/.github/workflows/reusable-check.yml +0 -0
  10. {krx_openapi-0.2.0 → krx_openapi-0.2.1}/.gitignore +0 -0
  11. {krx_openapi-0.2.0 → krx_openapi-0.2.1}/LICENSE +0 -0
  12. {krx_openapi-0.2.0 → krx_openapi-0.2.1}/Makefile +0 -0
  13. {krx_openapi-0.2.0 → krx_openapi-0.2.1}/README.en.md +0 -0
  14. {krx_openapi-0.2.0 → krx_openapi-0.2.1}/README.md +0 -0
  15. {krx_openapi-0.2.0 → krx_openapi-0.2.1}/plugins/krx/.claude-plugin/plugin.json +0 -0
  16. {krx_openapi-0.2.0 → krx_openapi-0.2.1}/plugins/krx/.codex-plugin/plugin.json +0 -0
  17. {krx_openapi-0.2.0 → krx_openapi-0.2.1}/plugins/krx/skills/fetch/SKILL.md +0 -0
  18. {krx_openapi-0.2.0 → krx_openapi-0.2.1}/plugins/krx/skills/fields/SKILL.md +0 -0
  19. {krx_openapi-0.2.0 → krx_openapi-0.2.1}/plugins/krx/skills/list/SKILL.md +0 -0
  20. {krx_openapi-0.2.0 → krx_openapi-0.2.1}/src/krx_openapi/__init__.py +0 -0
  21. {krx_openapi-0.2.0 → krx_openapi-0.2.1}/src/krx_openapi/__main__.py +0 -0
  22. {krx_openapi-0.2.0 → krx_openapi-0.2.1}/src/krx_openapi/_config.py +0 -0
  23. {krx_openapi-0.2.0 → krx_openapi-0.2.1}/src/krx_openapi/_endpoint.py +0 -0
  24. {krx_openapi-0.2.0 → krx_openapi-0.2.1}/src/krx_openapi/catalog.py +0 -0
  25. {krx_openapi-0.2.0 → krx_openapi-0.2.1}/src/krx_openapi/cli.py +0 -0
  26. {krx_openapi-0.2.0 → krx_openapi-0.2.1}/src/krx_openapi/client.py +0 -0
  27. {krx_openapi-0.2.0 → krx_openapi-0.2.1}/src/krx_openapi/errors.py +0 -0
  28. {krx_openapi-0.2.0 → krx_openapi-0.2.1}/src/krx_openapi/py.typed +0 -0
  29. {krx_openapi-0.2.0 → krx_openapi-0.2.1}/src/krx_openapi/types.py +0 -0
  30. {krx_openapi-0.2.0 → krx_openapi-0.2.1}/tests/__init__.py +0 -0
  31. {krx_openapi-0.2.0 → krx_openapi-0.2.1}/tests/conftest.py +0 -0
  32. {krx_openapi-0.2.0 → krx_openapi-0.2.1}/tests/test_catalog.py +0 -0
  33. {krx_openapi-0.2.0 → krx_openapi-0.2.1}/tests/test_cli.py +0 -0
  34. {krx_openapi-0.2.0 → krx_openapi-0.2.1}/tests/test_client.py +0 -0
  35. {krx_openapi-0.2.0 → krx_openapi-0.2.1}/tests/test_config.py +0 -0
  36. {krx_openapi-0.2.0 → krx_openapi-0.2.1}/tests/test_endpoints.py +0 -0
  37. {krx_openapi-0.2.0 → krx_openapi-0.2.1}/tests/test_errors.py +0 -0
  38. {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.0
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.0"
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
- raise KRXRateLimitError(
114
+ failure = KRXRateLimitError(
109
115
  "KRX daily call quota exceeded (10,000/key/day); resets at "
110
- "midnight") from err
111
- hint = ""
112
- if err.code == _FORBIDDEN_STATUS:
113
- hint = (" -- check the key, that the URL is https and /svc/apis "
114
- "(not /svc/sample/apis with a real key)")
115
- raise KRXNetworkError(f"HTTP {err.code} from KRX for {url}{hint}") from err
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
- raise KRXNetworkError(f"request to KRX failed: {err.reason}") from err
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
- raise KRXNetworkError(f"KRX response read failed for {url}: {err}") from err
122
- return _rows_from_body(raw, url)
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