quantdb-sdk 0.4.6__tar.gz → 0.4.8__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 (41) hide show
  1. {quantdb_sdk-0.4.6 → quantdb_sdk-0.4.8}/CHANGELOG.md +28 -0
  2. {quantdb_sdk-0.4.6/quantdb_sdk.egg-info → quantdb_sdk-0.4.8}/PKG-INFO +1 -1
  3. {quantdb_sdk-0.4.6 → quantdb_sdk-0.4.8}/pyproject.toml +1 -1
  4. {quantdb_sdk-0.4.6 → quantdb_sdk-0.4.8}/quantdb_sdk/__main__.py +36 -10
  5. {quantdb_sdk-0.4.6 → quantdb_sdk-0.4.8}/quantdb_sdk/async_client.py +5 -6
  6. {quantdb_sdk-0.4.6 → quantdb_sdk-0.4.8}/quantdb_sdk/client.py +11 -17
  7. {quantdb_sdk-0.4.6 → quantdb_sdk-0.4.8/quantdb_sdk.egg-info}/PKG-INFO +1 -1
  8. {quantdb_sdk-0.4.6 → quantdb_sdk-0.4.8}/quantdb_sdk.egg-info/SOURCES.txt +1 -0
  9. quantdb_sdk-0.4.8/tests/test_cli_encoding.py +92 -0
  10. {quantdb_sdk-0.4.6 → quantdb_sdk-0.4.8}/LICENSE +0 -0
  11. {quantdb_sdk-0.4.6 → quantdb_sdk-0.4.8}/MANIFEST.in +0 -0
  12. {quantdb_sdk-0.4.6 → quantdb_sdk-0.4.8}/README.md +0 -0
  13. {quantdb_sdk-0.4.6 → quantdb_sdk-0.4.8}/quantdb_sdk/__init__.py +0 -0
  14. {quantdb_sdk-0.4.6 → quantdb_sdk-0.4.8}/quantdb_sdk/_utils.py +0 -0
  15. {quantdb_sdk-0.4.6 → quantdb_sdk-0.4.8}/quantdb_sdk/errors.py +0 -0
  16. {quantdb_sdk-0.4.6 → quantdb_sdk-0.4.8}/quantdb_sdk/examples/__init__.py +0 -0
  17. {quantdb_sdk-0.4.6 → quantdb_sdk-0.4.8}/quantdb_sdk/examples/demo_01_connect_basics.py +0 -0
  18. {quantdb_sdk-0.4.6 → quantdb_sdk-0.4.8}/quantdb_sdk/examples/demo_02_kline_v2_daily.py +0 -0
  19. {quantdb_sdk-0.4.6 → quantdb_sdk-0.4.8}/quantdb_sdk/examples/demo_03_sync_full_market.py +0 -0
  20. {quantdb_sdk-0.4.6 → quantdb_sdk-0.4.8}/quantdb_sdk/examples/demo_04_duckdb_warehouse.py +0 -0
  21. {quantdb_sdk-0.4.6 → quantdb_sdk-0.4.8}/quantdb_sdk/examples/demo_05_read_local_data.py +0 -0
  22. {quantdb_sdk-0.4.6 → quantdb_sdk-0.4.8}/quantdb_sdk/examples/demo_06_tick_and_factors.py +0 -0
  23. {quantdb_sdk-0.4.6 → quantdb_sdk-0.4.8}/quantdb_sdk/examples/demo_07_data_hub.py +0 -0
  24. {quantdb_sdk-0.4.6 → quantdb_sdk-0.4.8}/quantdb_sdk/hub.py +0 -0
  25. {quantdb_sdk-0.4.6 → quantdb_sdk-0.4.8}/quantdb_sdk/layout.py +0 -0
  26. {quantdb_sdk-0.4.6 → quantdb_sdk-0.4.8}/quantdb_sdk/mcp_download.py +0 -0
  27. {quantdb_sdk-0.4.6 → quantdb_sdk-0.4.8}/quantdb_sdk/py.typed +0 -0
  28. {quantdb_sdk-0.4.6 → quantdb_sdk-0.4.8}/quantdb_sdk.egg-info/dependency_links.txt +0 -0
  29. {quantdb_sdk-0.4.6 → quantdb_sdk-0.4.8}/quantdb_sdk.egg-info/entry_points.txt +0 -0
  30. {quantdb_sdk-0.4.6 → quantdb_sdk-0.4.8}/quantdb_sdk.egg-info/requires.txt +0 -0
  31. {quantdb_sdk-0.4.6 → quantdb_sdk-0.4.8}/quantdb_sdk.egg-info/top_level.txt +0 -0
  32. {quantdb_sdk-0.4.6 → quantdb_sdk-0.4.8}/setup.cfg +0 -0
  33. {quantdb_sdk-0.4.6 → quantdb_sdk-0.4.8}/tests/test_async_client.py +0 -0
  34. {quantdb_sdk-0.4.6 → quantdb_sdk-0.4.8}/tests/test_client.py +0 -0
  35. {quantdb_sdk-0.4.6 → quantdb_sdk-0.4.8}/tests/test_data_hub.py +0 -0
  36. {quantdb_sdk-0.4.6 → quantdb_sdk-0.4.8}/tests/test_hub_models.py +0 -0
  37. {quantdb_sdk-0.4.6 → quantdb_sdk-0.4.8}/tests/test_layout.py +0 -0
  38. {quantdb_sdk-0.4.6 → quantdb_sdk-0.4.8}/tests/test_mcp_download.py +0 -0
  39. {quantdb_sdk-0.4.6 → quantdb_sdk-0.4.8}/tests/test_technical_indicators.py +0 -0
  40. {quantdb_sdk-0.4.6 → quantdb_sdk-0.4.8}/tests/test_update_pipeline.py +0 -0
  41. {quantdb_sdk-0.4.6 → quantdb_sdk-0.4.8}/tests/test_utils.py +0 -0
@@ -3,6 +3,34 @@
3
3
  所有 notable 变更都会记录在此文件。格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),
4
4
  版本号遵循 [Semantic Versioning](https://semver.org/lang/zh-CN/)。
5
5
 
6
+ ## \[0.4.8] - 2026-10-27
7
+
8
+ ### Fixed
9
+
10
+ - **`quantdb usage` 在中文 Windows 控制台崩溃**:`余额` 一行输出的是窄人民币符号 U+00A5,而 Windows 中文控制台默认编码是 GBK/cp936,**GBK 中不存在该字符**(GBK 用 0x80 表示),`print` 抛 `UnicodeEncodeError: 'gbk' codec can't encode character '\xa5'`,命令以异常收场、后续输出(订阅状态等)全部丢失。
11
+ 修复两处:① 金额改用**全角人民币符号 U+FFE5**(GBK 可编码,中文终端显示正常);② CLI 入口新增 `_configure_stdout()`,把 stdout/stderr 的编码错误处理降级为 `replace`——以后任何不可编码字符只会显示为 `?`,不再中断命令。
12
+
13
+ ### Added
14
+
15
+ - 新增回归测试 `tests/test_cli_encoding.py`(仓库根 `tests/`,不进 wheel):
16
+ - 扫描 SDK 全部源码的**字符串字面量**,断言每个字符都能被 GBK 编码——防止再次引入不可编码字符;
17
+ - 在 GBK 编码的 stdout 下渲染用量输出,断言不抛异常;
18
+ - 断言 `_configure_stdout()` 确实把编码错误处理改为 `replace`。
19
+
20
+ ## \[0.4.7] - 2026-10-27
21
+
22
+ ### Fixed
23
+
24
+ - **适配下载 302 单跳直连**:服务端已取消「一次性下载令牌 + `/api/v1/data/cdn-bridge` 中转」链路,`/api/v1/data/download` 预扣成功后直接 302 到 CDN 签名 URL(单跳)。同步/异步客户端原有的两跳跟随假设已相应调整:
25
+ - 新增 **CDN 专用裸会话**(`QuantDBClient._cdn_session`):直连 CDN 时**不携带 API Key / JWT**,避免鉴权头泄露到 CDN 域;同一会话跨文件复用 TLS 连接,避免逐文件新建连接反复握手。
26
+ - 手动跟随重定向并保留每跳的 connect timeout 重试,网络抖动不会中断整个同步。
27
+
28
+ ### Changed
29
+
30
+ - 直连 CDN 模式**签发即按对象大小计费、不再退款**(服务端无法观测直连传输的真实字节,退款只能依赖客户端自报,不可信)。需要按真实传输计费、断点续传可退款的用户,应在请求头发送 `X-QuantDB-Delivery: proxy` 走网关代理模式(网关可观测字节并按未发送部分退款)。
31
+
32
+ > 兼容性:0.4.7 对接 2026-10-27 上线的服务端版本;旧服务端(两跳 cdn-bridge)仍可正常同步,无需回退。
33
+
6
34
  ## \[0.4.6] - 2026-10-27
7
35
 
8
36
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quantdb-sdk
3
- Version: 0.4.6
3
+ Version: 0.4.8
4
4
  Summary: QuantDB 量化数据平台官方 Python SDK
5
5
  Author: QuantDB Team
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "quantdb-sdk"
7
- version = "0.4.6"
7
+ version = "0.4.8"
8
8
  description = "QuantDB 量化数据平台官方 Python SDK"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -63,7 +63,42 @@ def cmd_datasets(name: Optional[str] = None) -> int:
63
63
  return 0
64
64
 
65
65
 
66
+ def _configure_stdout() -> None:
67
+ """让 CLI 在非 UTF-8 控制台(中文 Windows 默认 GBK/cp936)下不因不可编码字符崩溃。
68
+
69
+ Windows 中文控制台的默认编码是 GBK,而窄人民币符号 U+00A5 在 GBK 中**不存在**
70
+ (GBK 用 0x80 表示该符号),直接 print 会抛
71
+ ``UnicodeEncodeError: 'gbk' codec can't encode character '\\xa5'``,
72
+ 导致整条命令以异常收场、后续输出全部丢失。
73
+
74
+ 这里把 stdout/stderr 的编码错误处理改成 replace:无法编码的字符降级为 '?',
75
+ 命令本身不再中断。配合正文中改用全角 ¥(U+FFE5,GBK 可编码),
76
+ 中文 Windows 终端可正常显示金额。
77
+ """
78
+ for stream in (sys.stdout, sys.stderr):
79
+ try:
80
+ stream.reconfigure(errors="replace") # type: ignore[attr-defined]
81
+ except Exception: # pragma: no cover - 老版本/非 TextIOWrapper 忽略
82
+ pass
83
+
84
+
85
+ def print_usage(usage: dict) -> None:
86
+ """打印账户用量(抽成独立函数便于单测覆盖非 UTF-8 控制台场景)。"""
87
+ print("账户用量:")
88
+ print(f" 套餐: 已用 {usage['plan_used_gb']:.2f} GB / {usage['plan_total_gb']:.2f} GB"
89
+ f"(剩余 {usage['plan_remaining_gb']:.2f} GB)")
90
+ print(f" 流量包: 已用 {usage['package_used_gb']:.2f} GB / {usage['package_total_gb']:.2f} GB"
91
+ f"(剩余 {usage['package_remaining_gb']:.2f} GB)")
92
+ print(f" 合计: 已用 {usage['used_gb']:.2f} GB / {usage['total_gb']:.2f} GB"
93
+ f"(剩余 {usage['remaining_gb']:.2f} GB)")
94
+ # 全角人民币符号 U+FFE5:GBK 可编码;窄符号 U+00A5 在 GBK 中不存在会崩溃
95
+ print(f" 余额: ¥{usage['balance_yuan']:.2f}")
96
+ sub = usage.get("subscription", {})
97
+ print(f" 订阅: {sub.get('status', 'none')}")
98
+
99
+
66
100
  def main() -> int:
101
+ _configure_stdout()
67
102
  parser = argparse.ArgumentParser(
68
103
  prog="quantdb",
69
104
  description="QuantDB 量化数据平台官方 Python SDK",
@@ -158,16 +193,7 @@ def main() -> int:
158
193
 
159
194
  if args.command == "usage":
160
195
  usage = client.get_usage()
161
- print(f"账户用量:")
162
- print(f" 套餐: 已用 {usage['plan_used_gb']:.2f} GB / {usage['plan_total_gb']:.2f} GB"
163
- f"(剩余 {usage['plan_remaining_gb']:.2f} GB)")
164
- print(f" 流量包: 已用 {usage['package_used_gb']:.2f} GB / {usage['package_total_gb']:.2f} GB"
165
- f"(剩余 {usage['package_remaining_gb']:.2f} GB)")
166
- print(f" 合计: 已用 {usage['used_gb']:.2f} GB / {usage['total_gb']:.2f} GB"
167
- f"(剩余 {usage['remaining_gb']:.2f} GB)")
168
- print(f" 余额: ¥{usage['balance_yuan']:.2f}")
169
- sub = usage.get("subscription", {})
170
- print(f" 订阅: {sub.get('status', 'none')}")
196
+ print_usage(usage)
171
197
  return 0
172
198
 
173
199
  if args.command == "sync":
@@ -28,7 +28,7 @@ from .errors import (
28
28
  )
29
29
 
30
30
  # 维护提示:每次发版必须与 pyproject.toml 版本号同步
31
- _USER_AGENT = "QuantDB-Python-SDK/0.4.6"
31
+ _USER_AGENT = "QuantDB-Python-SDK/0.4.8"
32
32
 
33
33
 
34
34
  def _etag_hex(value: Optional[str]) -> Optional[str]:
@@ -139,8 +139,8 @@ class AsyncQuantDBClient:
139
139
  裸客户端直连 Location,鉴权头不会泄露给 CDN 域。
140
140
 
141
141
  重定向链路(CDN 直连模式):
142
- /download 302→ /cdn-bridge?token=xxx 302→ https://cdn.xxx/key?sign=...
143
- 两跳均手动跟随,每跳带重试(connect timeout 网络抖动容错)。
142
+ /download 302→ https://cdn.xxx/key?sign=...(单跳)
143
+ 手动跟随,每跳带重试(connect timeout 网络抖动容错)。
144
144
  """
145
145
  async with self.client.stream(
146
146
  "GET", f"{self.api_host}/api/v1/data/download", params=params, headers=headers
@@ -166,9 +166,8 @@ class AsyncQuantDBClient:
166
166
  ) -> httpx.Response:
167
167
  """手动跟随 302 重定向链路,每跳带重试。
168
168
 
169
- cdn-bridge 和 CDN 直连请求均通过此方法,connect timeout 网络抖动
170
- 时自动重试,避免一次性 token(90s 有效期)场景下单次抖动导致整个
171
- sync 中断。
169
+ CDN 直连请求通过此方法,connect timeout 网络抖动时自动重试,避免
170
+ 单次抖动导致整个 sync 中断。
172
171
 
173
172
  返回已进入 stream 模式的 httpx.Response,调用方需用 async with 管理。
174
173
  """
@@ -32,7 +32,7 @@ from .errors import (
32
32
  )
33
33
 
34
34
  # 维护提示:每次发版必须与 pyproject.toml 版本号同步
35
- _USER_AGENT = "QuantDB-Python-SDK/0.4.6"
35
+ _USER_AGENT = "QuantDB-Python-SDK/0.4.8"
36
36
 
37
37
 
38
38
  def _etag_hex(value: Optional[str]) -> Optional[str]:
@@ -75,6 +75,9 @@ class QuantDBClient:
75
75
  self.headers: Dict[str, str] = {}
76
76
  self.session = requests.Session()
77
77
  self.session.headers.update({"User-Agent": _USER_AGENT})
78
+ # CDN 直连专用会话:不携带 API Key / JWT,且跨文件复用 TLS 连接(逐文件新建连接会反复握手)。
79
+ self._cdn_session = requests.Session()
80
+ self._cdn_session.headers.update({"User-Agent": _USER_AGENT})
78
81
 
79
82
  if api_key:
80
83
  self.headers = {"X-API-Key": api_key}
@@ -168,13 +171,10 @@ class QuantDBClient:
168
171
  def _download_stream(self, params: dict, headers: Optional[dict] = None) -> requests.Response:
169
172
  """下载专用流式请求:手动处理服务端 302 CDN 直连跳转。
170
173
 
171
- 服务端开启 CDN 直连后,/api/v1/data/download 预扣流量成功时返回
172
- 302 -> CDN 签名 URL。requests 自动跟随会把自定义 X-API-Key 头
173
- 透传给 CDN 域,故禁用自动跟随,改用不带鉴权头的裸请求直连。
174
-
175
- 重定向链路(CDN 直连模式):
176
- /download 302→ /cdn-bridge?token=xxx 302→ https://cdn.xxx/key?sign=...
177
- 两跳均手动跟随,每跳带重试(connect timeout 网络抖动容错)。
174
+ 服务端开启 CDN 直连后,/api/v1/data/download 预扣流量成功时直接返回
175
+ 302 -> CDN 签名 URL(单跳)。requests 自动跟随会把自定义 X-API-Key 头
176
+ 透传给 CDN 域,故禁用自动跟随,改用不带鉴权头的 CDN 专用会话直连。
177
+ 每跳带重试(connect timeout 网络抖动容错)。
178
178
  """
179
179
  resp = self._request(
180
180
  "GET", "/api/v1/data/download", params=params, stream=True,
@@ -200,21 +200,15 @@ class QuantDBClient:
200
200
  ) -> requests.Response:
201
201
  """手动跟随 302 重定向链路,每跳带重试。
202
202
 
203
- cdn-bridge 和 CDN 直连请求均通过此方法,connect timeout 网络抖动
204
- 时自动重试,避免一次性 token(90s 有效期)场景下单次抖动导致整个
205
- sync 中断。
203
+ CDN 直连请求通过此方法,connect timeout 网络抖动时自动重试,避免
204
+ 单次抖动导致整个 sync 中断。
206
205
  """
207
206
  current_etag = etag
208
207
  for _ in range(max_hops):
209
208
  last_exc = None
210
209
  for attempt in range(retries + 1):
211
210
  try:
212
- cdn_resp = requests.get(
213
- url,
214
- stream=True,
215
- timeout=self.timeout,
216
- headers={"User-Agent": _USER_AGENT},
217
- )
211
+ cdn_resp = self._cdn_session.get(url, stream=True, timeout=self.timeout)
218
212
  break
219
213
  except (requests.ConnectionError, requests.Timeout) as exc:
220
214
  last_exc = exc
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quantdb-sdk
3
- Version: 0.4.6
3
+ Version: 0.4.8
4
4
  Summary: QuantDB 量化数据平台官方 Python SDK
5
5
  Author: QuantDB Team
6
6
  License: MIT
@@ -28,6 +28,7 @@ quantdb_sdk/examples/demo_05_read_local_data.py
28
28
  quantdb_sdk/examples/demo_06_tick_and_factors.py
29
29
  quantdb_sdk/examples/demo_07_data_hub.py
30
30
  tests/test_async_client.py
31
+ tests/test_cli_encoding.py
31
32
  tests/test_client.py
32
33
  tests/test_data_hub.py
33
34
  tests/test_hub_models.py
@@ -0,0 +1,92 @@
1
+ """CLI 输出编码回归测试。
2
+
3
+ 背景:Windows 中文控制台默认编码是 GBK/cp936,而 U+00A5(窄人民币符号)在 GBK
4
+ 中不存在,`print` 会抛 UnicodeEncodeError,导致命令以异常收场、后续输出全部丢失
5
+ (0.4.8 修复:改用全角 U+FFE5 + stdout 错误处理降级为 replace)。
6
+
7
+ 本文件放在仓库根的 tests/ 下(不在 quantdb_sdk 包内),不会被打进 wheel。
8
+ """
9
+
10
+ import io
11
+ import pathlib
12
+ import sys
13
+ import tokenize
14
+
15
+ import pytest
16
+
17
+ SDK_DIR = pathlib.Path(__file__).resolve().parents[1] / "quantdb_sdk"
18
+
19
+
20
+ def _string_literal_chars(path: pathlib.Path):
21
+ """收集源码中所有字符串字面量(含 docstring)里出现过的字符。"""
22
+ source = path.read_text(encoding="utf-8")
23
+ chars = set()
24
+ for token in tokenize.generate_tokens(io.StringIO(source).readline):
25
+ if token.type == tokenize.STRING:
26
+ chars.update(token.string)
27
+ return chars
28
+
29
+
30
+ def test_sdk_string_literals_are_gbk_encodable():
31
+ """所有会被打印的字符串都必须能被 GBK 编码,否则中文 Windows 控制台会崩溃。"""
32
+ offenders = []
33
+ for path in sorted(SDK_DIR.rglob("*.py")):
34
+ for ch in _string_literal_chars(path):
35
+ try:
36
+ ch.encode("gbk")
37
+ except UnicodeEncodeError:
38
+ offenders.append((path.relative_to(SDK_DIR.parent).as_posix(), hex(ord(ch))))
39
+ assert not offenders, (
40
+ "以下字符在 GBK 控制台无法编码(会崩溃 CLI 输出): "
41
+ + ", ".join(f"{f}:{cp}" for f, cp in offenders)
42
+ )
43
+
44
+
45
+ def test_print_usage_under_gbk_stdout(monkeypatch, capsys):
46
+ """在 GBK 编码的 stdout 下打印用量,不得抛 UnicodeEncodeError。"""
47
+ from quantdb_sdk.__main__ import print_usage
48
+
49
+ buffer = io.BytesIO()
50
+ gbk_stdout = io.TextIOWrapper(buffer, encoding="gbk", errors="strict")
51
+ monkeypatch.setattr(sys, "stdout", gbk_stdout)
52
+
53
+ print_usage({
54
+ "plan_used_gb": 50.0, "plan_total_gb": 50.0, "plan_remaining_gb": 0.0,
55
+ "package_used_gb": 15.8, "package_total_gb": 110.0, "package_remaining_gb": 94.2,
56
+ "used_gb": 50.0, "total_gb": 160.0, "remaining_gb": 94.2,
57
+ "balance_yuan": 12.34,
58
+ "subscription": {"status": "active"},
59
+ })
60
+ gbk_stdout.flush()
61
+
62
+ out = buffer.getvalue().decode("gbk")
63
+ assert "账户用量" in out
64
+ assert "12.34" in out
65
+
66
+
67
+ def test_configure_stdout_downgrades_encoding_errors(monkeypatch):
68
+ """_configure_stdout 应把 stdout/stderr 的编码错误处理改为 replace。"""
69
+ from quantdb_sdk.__main__ import _configure_stdout
70
+
71
+ buffer = io.BytesIO()
72
+ stream = io.TextIOWrapper(buffer, encoding="gbk", errors="strict")
73
+ monkeypatch.setattr(sys, "stdout", stream)
74
+ monkeypatch.setattr(sys, "stderr", stream)
75
+
76
+ _configure_stdout()
77
+
78
+ assert stream.errors == "replace"
79
+ # 不可编码字符应降级为 '?' 而不是抛异常
80
+ stream.write("\u00a5")
81
+ stream.flush()
82
+ assert "?" in buffer.getvalue().decode("gbk")
83
+
84
+
85
+ def test_main_version_smoke():
86
+ """--version 走 argparse 的 version action,不应因控制台编码崩溃。"""
87
+ from quantdb_sdk.__main__ import main
88
+
89
+ with pytest.raises(SystemExit) as exc:
90
+ sys.argv = ["quantdb", "--version"]
91
+ main()
92
+ assert exc.value.code == 0
File without changes
File without changes
File without changes
File without changes