quantdb-sdk 0.2.7__tar.gz → 0.2.9__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.
- {quantdb_sdk-0.2.7 → quantdb_sdk-0.2.9}/CHANGELOG.md +7 -0
- {quantdb_sdk-0.2.7/quantdb_sdk.egg-info → quantdb_sdk-0.2.9}/PKG-INFO +4 -4
- {quantdb_sdk-0.2.7 → quantdb_sdk-0.2.9}/README.md +1 -1
- {quantdb_sdk-0.2.7 → quantdb_sdk-0.2.9}/pyproject.toml +3 -3
- {quantdb_sdk-0.2.7 → quantdb_sdk-0.2.9}/quantdb_sdk/async_client.py +66 -7
- {quantdb_sdk-0.2.7 → quantdb_sdk-0.2.9}/quantdb_sdk/client.py +59 -13
- {quantdb_sdk-0.2.7 → quantdb_sdk-0.2.9/quantdb_sdk.egg-info}/PKG-INFO +4 -4
- {quantdb_sdk-0.2.7 → quantdb_sdk-0.2.9}/LICENSE +0 -0
- {quantdb_sdk-0.2.7 → quantdb_sdk-0.2.9}/MANIFEST.in +0 -0
- {quantdb_sdk-0.2.7 → quantdb_sdk-0.2.9}/quantdb_sdk/__init__.py +0 -0
- {quantdb_sdk-0.2.7 → quantdb_sdk-0.2.9}/quantdb_sdk/__main__.py +0 -0
- {quantdb_sdk-0.2.7 → quantdb_sdk-0.2.9}/quantdb_sdk/_utils.py +0 -0
- {quantdb_sdk-0.2.7 → quantdb_sdk-0.2.9}/quantdb_sdk/errors.py +0 -0
- {quantdb_sdk-0.2.7 → quantdb_sdk-0.2.9}/quantdb_sdk/py.typed +0 -0
- {quantdb_sdk-0.2.7 → quantdb_sdk-0.2.9}/quantdb_sdk.egg-info/SOURCES.txt +0 -0
- {quantdb_sdk-0.2.7 → quantdb_sdk-0.2.9}/quantdb_sdk.egg-info/dependency_links.txt +0 -0
- {quantdb_sdk-0.2.7 → quantdb_sdk-0.2.9}/quantdb_sdk.egg-info/entry_points.txt +0 -0
- {quantdb_sdk-0.2.7 → quantdb_sdk-0.2.9}/quantdb_sdk.egg-info/requires.txt +0 -0
- {quantdb_sdk-0.2.7 → quantdb_sdk-0.2.9}/quantdb_sdk.egg-info/top_level.txt +0 -0
- {quantdb_sdk-0.2.7 → quantdb_sdk-0.2.9}/setup.cfg +0 -0
- {quantdb_sdk-0.2.7 → quantdb_sdk-0.2.9}/tests/test_async_client.py +0 -0
- {quantdb_sdk-0.2.7 → quantdb_sdk-0.2.9}/tests/test_client.py +0 -0
- {quantdb_sdk-0.2.7 → quantdb_sdk-0.2.9}/tests/test_technical_indicators.py +0 -0
- {quantdb_sdk-0.2.7 → quantdb_sdk-0.2.9}/tests/test_update_pipeline.py +0 -0
|
@@ -3,6 +3,13 @@
|
|
|
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.2.8] - 2026-07-30
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
- **CDN 直连下载 302 相对 URL 拼接**:服务端 CDN 直连模式(`QUANTDB_DOWNLOAD_REDIRECT`)的 Stage 1 302 返回相对 URL(`/api/v1/data/cdn-bridge?token=xxx`),同步/异步客户端此前直接将此相对路径作为请求 URL,导致 `requests`/`httpx` 无法解析(`MissingSchema`/`UnsupportedProtocol`),CDN 直连下载完全不可用。现在当 `Location` 以 `/` 开头时自动拼接 `api_host` 构造完整 URL。
|
|
10
|
+
- **CDN 直连请求无重试**:cdn-bridge 和 CDN 签名 URL 请求此前使用裸 `requests.get()`/`httpx.stream()` 调用,无重试、无超时容错。一次 connect timeout 或网络抖动即导致整个 `sync_dataset` 中断(且预扣流量不退)。现在两跳均带 2 次重试(指数退避 1s/2s),捕获 `ConnectionError`/`Timeout`/`ReadError` 自动重试。
|
|
11
|
+
- **302 重定向链路手动跟随**:此前只跟随第一跳 302,服务端两跳链路(`/download` → `/cdn-bridge?token=xxx` → CDN 签名 URL)的第二跳被忽略。现在统一用 `_follow_redirect_with_retry` 循环跟随最多 3 跳,ETag 逐跳透传。
|
|
12
|
+
|
|
6
13
|
## [0.2.7] - 2026-07-29
|
|
7
14
|
|
|
8
15
|
### Fixed
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quantdb-sdk
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.9
|
|
4
4
|
Summary: QuantDB 量化数据平台官方 Python SDK
|
|
5
5
|
Author: QuantDB Team
|
|
6
6
|
License: MIT
|
|
7
|
-
Project-URL: Homepage, https://
|
|
8
|
-
Project-URL: Documentation, https://
|
|
7
|
+
Project-URL: Homepage, https://quantdb.quantmind.cloud
|
|
8
|
+
Project-URL: Documentation, https://quantdb.quantmind.cloud/docs/sdk.html
|
|
9
9
|
Project-URL: Repository, https://github.com/quantdb/quantdb
|
|
10
10
|
Project-URL: Changelog, https://github.com/quantdb/quantdb/blob/main/CHANGELOG.md
|
|
11
11
|
Keywords: quant,finance,data,a-share,parquet
|
|
@@ -147,4 +147,4 @@ with ThreadPoolExecutor(max_workers=8) as pool:
|
|
|
147
147
|
|
|
148
148
|
## 文档
|
|
149
149
|
|
|
150
|
-
完整文档请访问:https://
|
|
150
|
+
完整文档请访问:https://quantdb.quantmind.cloud/docs/sdk.html
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "quantdb-sdk"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.9"
|
|
8
8
|
description = "QuantDB 量化数据平台官方 Python SDK"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -53,8 +53,8 @@ dev = [
|
|
|
53
53
|
]
|
|
54
54
|
|
|
55
55
|
[project.urls]
|
|
56
|
-
Homepage = "https://
|
|
57
|
-
Documentation = "https://
|
|
56
|
+
Homepage = "https://quantdb.quantmind.cloud"
|
|
57
|
+
Documentation = "https://quantdb.quantmind.cloud/docs/sdk.html"
|
|
58
58
|
Repository = "https://github.com/quantdb/quantdb"
|
|
59
59
|
Changelog = "https://github.com/quantdb/quantdb/blob/main/CHANGELOG.md"
|
|
60
60
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""QuantDB 异步 Python SDK(基于 httpx)。"""
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
import hashlib
|
|
4
5
|
import io
|
|
5
6
|
import os
|
|
@@ -27,7 +28,7 @@ from .errors import (
|
|
|
27
28
|
)
|
|
28
29
|
|
|
29
30
|
# 维护提示:每次发版必须与 pyproject.toml 版本号同步
|
|
30
|
-
_USER_AGENT = "QuantDB-Python-SDK/0.2.
|
|
31
|
+
_USER_AGENT = "QuantDB-Python-SDK/0.2.8"
|
|
31
32
|
|
|
32
33
|
|
|
33
34
|
class AsyncQuantDBClient:
|
|
@@ -81,6 +82,10 @@ class AsyncQuantDBClient:
|
|
|
81
82
|
服务端开启 CDN 直连后,/api/v1/data/download 预扣流量成功时返回
|
|
82
83
|
302 -> CDN 签名 URL。httpx 默认不跟随重定向;这里用不带鉴权头的
|
|
83
84
|
裸客户端直连 Location,鉴权头不会泄露给 CDN 域。
|
|
85
|
+
|
|
86
|
+
重定向链路(CDN 直连模式):
|
|
87
|
+
/download 302→ /cdn-bridge?token=xxx 302→ https://cdn.xxx/key?sign=...
|
|
88
|
+
两跳均手动跟随,每跳带重试(connect timeout 网络抖动容错)。
|
|
84
89
|
"""
|
|
85
90
|
async with self.client.stream(
|
|
86
91
|
"GET", f"{self.api_host}/api/v1/data/download", params=params, headers=headers
|
|
@@ -92,14 +97,68 @@ class AsyncQuantDBClient:
|
|
|
92
97
|
etag = resp.headers.get("ETag", "")
|
|
93
98
|
if not location:
|
|
94
99
|
raise ServerError("下载重定向缺少 Location 头")
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
100
|
+
# 服务端 302 可能返回相对 URL(如 /api/v1/data/cdn-bridge?token=xxx),
|
|
101
|
+
# 需拼接 api_host 构造完整 URL,否则 httpx 无法解析。
|
|
102
|
+
if location.startswith("/"):
|
|
103
|
+
location = f"{self.api_host}{location}"
|
|
104
|
+
# 手动跟随重定向链路(最多 3 跳),每跳带重试
|
|
105
|
+
cdn_resp = await self._follow_redirect_with_retry(location, etag, max_hops=3)
|
|
106
|
+
async with cdn_resp:
|
|
101
107
|
yield cdn_resp
|
|
102
108
|
|
|
109
|
+
async def _follow_redirect_with_retry(
|
|
110
|
+
self, url: str, etag: str, max_hops: int = 3, retries: int = 2,
|
|
111
|
+
) -> httpx.Response:
|
|
112
|
+
"""手动跟随 302 重定向链路,每跳带重试。
|
|
113
|
+
|
|
114
|
+
cdn-bridge 和 CDN 直连请求均通过此方法,connect timeout 网络抖动
|
|
115
|
+
时自动重试,避免一次性 token(90s 有效期)场景下单次抖动导致整个
|
|
116
|
+
sync 中断。
|
|
117
|
+
|
|
118
|
+
返回已进入 stream 模式的 httpx.Response,调用方需用 async with 管理。
|
|
119
|
+
"""
|
|
120
|
+
import asyncio
|
|
121
|
+
current_etag = etag
|
|
122
|
+
for _ in range(max_hops):
|
|
123
|
+
last_exc: Exception | None = None
|
|
124
|
+
cdn_resp: httpx.Response | None = None
|
|
125
|
+
for attempt in range(retries + 1):
|
|
126
|
+
try:
|
|
127
|
+
# httpx stream() 是 async context manager,需 __aenter__ 进入
|
|
128
|
+
ctx = self._bare_client.stream("GET", url)
|
|
129
|
+
cdn_resp = await ctx.__aenter__()
|
|
130
|
+
# 触发实际连接(访问 status_code 即触发 head 读取)
|
|
131
|
+
_ = cdn_resp.status_code
|
|
132
|
+
break
|
|
133
|
+
except (httpx.ConnectError, httpx.ConnectTimeout, httpx.ReadError, httpx.ReadTimeout) as exc:
|
|
134
|
+
if cdn_resp is not None:
|
|
135
|
+
await cdn_resp.aclose()
|
|
136
|
+
cdn_resp = None
|
|
137
|
+
last_exc = exc
|
|
138
|
+
if attempt < retries:
|
|
139
|
+
await asyncio.sleep(1.0 * (attempt + 1))
|
|
140
|
+
else:
|
|
141
|
+
raise ServerError(f"CDN 直连下载失败(重试 {retries} 次后仍网络错误):{last_exc}")
|
|
142
|
+
if cdn_resp.status_code not in (200, 301, 302, 303, 307, 308):
|
|
143
|
+
await cdn_resp.aclose()
|
|
144
|
+
raise ServerError(f"CDN 直连下载失败:HTTP {cdn_resp.status_code}")
|
|
145
|
+
if cdn_resp.status_code == 200:
|
|
146
|
+
# ETag 回填:网关 302 携带的 COS ETag → CDN 响应缺失时补上
|
|
147
|
+
if current_etag and not cdn_resp.headers.get("ETag"):
|
|
148
|
+
cdn_resp.headers["ETag"] = current_etag
|
|
149
|
+
return cdn_resp
|
|
150
|
+
# 继续跟随 302
|
|
151
|
+
next_url = cdn_resp.headers.get("Location", "")
|
|
152
|
+
next_etag = cdn_resp.headers.get("ETag", current_etag)
|
|
153
|
+
await cdn_resp.aclose()
|
|
154
|
+
if not next_url:
|
|
155
|
+
raise ServerError("重定向缺少 Location 头")
|
|
156
|
+
if next_url.startswith("/"):
|
|
157
|
+
next_url = f"{self.api_host}{next_url}"
|
|
158
|
+
url = next_url
|
|
159
|
+
current_etag = next_etag
|
|
160
|
+
raise ServerError("重定向链路过深(超过 3 跳)")
|
|
161
|
+
|
|
103
162
|
@staticmethod
|
|
104
163
|
def _validate_layout(layout: str) -> Literal["auto", "v1", "v2"]:
|
|
105
164
|
if layout not in {"auto", "v1", "v2"}:
|
|
@@ -6,6 +6,7 @@ import os
|
|
|
6
6
|
import re
|
|
7
7
|
import sqlite3
|
|
8
8
|
import glob
|
|
9
|
+
import time
|
|
9
10
|
from typing import Any, Dict, List, Optional, Literal
|
|
10
11
|
|
|
11
12
|
import pandas as pd
|
|
@@ -29,7 +30,7 @@ from .errors import (
|
|
|
29
30
|
)
|
|
30
31
|
|
|
31
32
|
# 维护提示:每次发版必须与 pyproject.toml 版本号同步
|
|
32
|
-
_USER_AGENT = "QuantDB-Python-SDK/0.2.
|
|
33
|
+
_USER_AGENT = "QuantDB-Python-SDK/0.2.8"
|
|
33
34
|
|
|
34
35
|
|
|
35
36
|
class QuantDBClient:
|
|
@@ -124,6 +125,10 @@ class QuantDBClient:
|
|
|
124
125
|
服务端开启 CDN 直连后,/api/v1/data/download 预扣流量成功时返回
|
|
125
126
|
302 -> CDN 签名 URL。requests 自动跟随会把自定义 X-API-Key 头
|
|
126
127
|
透传给 CDN 域,故禁用自动跟随,改用不带鉴权头的裸请求直连。
|
|
128
|
+
|
|
129
|
+
重定向链路(CDN 直连模式):
|
|
130
|
+
/download 302→ /cdn-bridge?token=xxx 302→ https://cdn.xxx/key?sign=...
|
|
131
|
+
两跳均手动跟随,每跳带重试(connect timeout 网络抖动容错)。
|
|
127
132
|
"""
|
|
128
133
|
resp = self._request(
|
|
129
134
|
"GET", "/api/v1/data/download", params=params, stream=True,
|
|
@@ -136,20 +141,61 @@ class QuantDBClient:
|
|
|
136
141
|
resp.close()
|
|
137
142
|
if not location:
|
|
138
143
|
raise ServerError("下载重定向缺少 Location 头")
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
)
|
|
145
|
-
if cdn_resp.status_code != 200:
|
|
146
|
-
cdn_resp.close()
|
|
147
|
-
raise ServerError(f"CDN 直连下载失败:HTTP {cdn_resp.status_code}")
|
|
148
|
-
# 网关 302 响应携带 COS ETag;若 CDN 响应缺失则回填,保证客户端缓存逻辑一致。
|
|
149
|
-
if etag and not cdn_resp.headers.get("ETag"):
|
|
150
|
-
cdn_resp.headers["ETag"] = etag
|
|
144
|
+
# 服务端 302 可能返回相对 URL(如 /api/v1/data/cdn-bridge?token=xxx),
|
|
145
|
+
# 需拼接 api_host 构造完整 URL,否则 requests.get 无法解析。
|
|
146
|
+
if location.startswith("/"):
|
|
147
|
+
location = f"{self.api_host}{location}"
|
|
148
|
+
# 手动跟随重定向链路(最多 3 跳),每跳带重试
|
|
149
|
+
cdn_resp = self._follow_redirect_with_retry(location, etag, max_hops=3)
|
|
151
150
|
return cdn_resp
|
|
152
151
|
|
|
152
|
+
def _follow_redirect_with_retry(
|
|
153
|
+
self, url: str, etag: str, max_hops: int = 3, retries: int = 2,
|
|
154
|
+
) -> requests.Response:
|
|
155
|
+
"""手动跟随 302 重定向链路,每跳带重试。
|
|
156
|
+
|
|
157
|
+
cdn-bridge 和 CDN 直连请求均通过此方法,connect timeout 网络抖动
|
|
158
|
+
时自动重试,避免一次性 token(90s 有效期)场景下单次抖动导致整个
|
|
159
|
+
sync 中断。
|
|
160
|
+
"""
|
|
161
|
+
current_etag = etag
|
|
162
|
+
for _ in range(max_hops):
|
|
163
|
+
last_exc = None
|
|
164
|
+
for attempt in range(retries + 1):
|
|
165
|
+
try:
|
|
166
|
+
cdn_resp = requests.get(
|
|
167
|
+
url,
|
|
168
|
+
stream=True,
|
|
169
|
+
timeout=self.timeout,
|
|
170
|
+
headers={"User-Agent": _USER_AGENT},
|
|
171
|
+
)
|
|
172
|
+
break
|
|
173
|
+
except (requests.ConnectionError, requests.Timeout) as exc:
|
|
174
|
+
last_exc = exc
|
|
175
|
+
if attempt < retries:
|
|
176
|
+
time.sleep(1.0 * (attempt + 1))
|
|
177
|
+
else:
|
|
178
|
+
raise ServerError(f"CDN 直连下载失败(重试 {retries} 次后仍网络错误):{last_exc}")
|
|
179
|
+
if cdn_resp.status_code not in (200, 301, 302, 303, 307, 308):
|
|
180
|
+
cdn_resp.close()
|
|
181
|
+
raise ServerError(f"CDN 直连下载失败:HTTP {cdn_resp.status_code}")
|
|
182
|
+
if cdn_resp.status_code == 200:
|
|
183
|
+
# ETag 回填:网关 302 携带的 COS ETag → CDN 响应缺失时补上
|
|
184
|
+
if current_etag and not cdn_resp.headers.get("ETag"):
|
|
185
|
+
cdn_resp.headers["ETag"] = current_etag
|
|
186
|
+
return cdn_resp
|
|
187
|
+
# 继续跟随 302
|
|
188
|
+
next_url = cdn_resp.headers.get("Location", "")
|
|
189
|
+
next_etag = cdn_resp.headers.get("ETag", current_etag)
|
|
190
|
+
cdn_resp.close()
|
|
191
|
+
if not next_url:
|
|
192
|
+
raise ServerError("重定向缺少 Location 头")
|
|
193
|
+
if next_url.startswith("/"):
|
|
194
|
+
next_url = f"{self.api_host}{next_url}"
|
|
195
|
+
url = next_url
|
|
196
|
+
current_etag = next_etag
|
|
197
|
+
raise ServerError("重定向链路过深(超过 3 跳)")
|
|
198
|
+
|
|
153
199
|
def _post(self, path: str, json: Optional[dict] = None) -> dict:
|
|
154
200
|
resp = self._request("POST", path, json=json)
|
|
155
201
|
return self._check_response(resp)
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quantdb-sdk
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.9
|
|
4
4
|
Summary: QuantDB 量化数据平台官方 Python SDK
|
|
5
5
|
Author: QuantDB Team
|
|
6
6
|
License: MIT
|
|
7
|
-
Project-URL: Homepage, https://
|
|
8
|
-
Project-URL: Documentation, https://
|
|
7
|
+
Project-URL: Homepage, https://quantdb.quantmind.cloud
|
|
8
|
+
Project-URL: Documentation, https://quantdb.quantmind.cloud/docs/sdk.html
|
|
9
9
|
Project-URL: Repository, https://github.com/quantdb/quantdb
|
|
10
10
|
Project-URL: Changelog, https://github.com/quantdb/quantdb/blob/main/CHANGELOG.md
|
|
11
11
|
Keywords: quant,finance,data,a-share,parquet
|
|
@@ -147,4 +147,4 @@ with ThreadPoolExecutor(max_workers=8) as pool:
|
|
|
147
147
|
|
|
148
148
|
## 文档
|
|
149
149
|
|
|
150
|
-
完整文档请访问:https://
|
|
150
|
+
完整文档请访问:https://quantdb.quantmind.cloud/docs/sdk.html
|
|
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
|