批量下载
需要一个发布版本的大段历史时,先准备一份 Parquet 快照,再凭票据分块下载,而不是逐页查询。服务未启用批量交付时,两个接口都返回 404 not-found。
1. 准备快照
POST /v1/datasets/{datasetId}/preparations,请求体:
{
"releaseId": "<发布版本 ID>",
"where": [
{"field": "trading_date", "op": "gte", "value": "2026-01-05"},
{"field": "trading_date", "op": "lte", "value": "2026-01-09"}
]
}
releaseId必填,取GET /v1/datasets/{datasetId}返回的release.releaseId;对已完成当前版本切换的数据集只接受当前版本。where可省略,表示该版本的全部分区;只支持按该版本的业务日期字段选择分区。交付的总是完整分区,不支持select与游标。
响应中的成员:
| 成员 | 说明 |
|---|---|
manifest | 交付清单:formatVersion(固定为 2)、datasetId、releaseId(版本根哈希)、contractFingerprint、fields(契约字段,列名保持原名)、encoding.format(固定为 parquet)、objects、partitionRule、keys、claim、lineage;有交易日期分区时另带 dateRange{start,end}。 |
manifest.objects | 每个文件的 sha256、bytes、rows 与 partition,按 partition 排序。 |
manifest.lineage | 可核实的出处:releaseFingerprint(版本根哈希)、source、rulesSha256、planSha256、outputSha256、lineageSha256、rowCount、inputRowCount。它是出处描述,不参与快照身份。 |
manifest.claim | 覆盖声明:complete、partial、external_blocked 或 unknown,原样转出根清单。只有 claim 为 complete 时,覆盖区间内的空才是已核实的无记录。 |
snapshotFingerprint | 快照身份,只由内容决定;组成与重算见下一节。 |
ticket | 下载票据,15 分钟内有效。 |
maxChunkBytes | 单次下载的字节上限,为 1048576(1 MiB)。 |
快照身份
snapshotFingerprint 是 sha256(canonical {datasetId, contractFingerprint, objects}),其中 objects 只取按 partition 排序的 {partition, sha256}。它不含版本根哈希、claim、lineage、dateRange、字节数、行数、构建号、时间或路径;同一组分区在不同版本里保留下来时得到同一身份,消费方不必重新下载或重新冻结。
消费方可以只凭 manifest 用标准库重算并核对:
import hashlib
import json
body = {
'datasetId': manifest['datasetId'],
'contractFingerprint': manifest['contractFingerprint'],
'objects': [{'partition': item['partition'], 'sha256': item['sha256']}
for item in sorted(manifest['objects'], key=lambda item: item['partition'])],
}
digest = hashlib.sha256(json.dumps(body, ensure_ascii=False, sort_keys=True,
separators=(',', ':'), allow_nan=False).encode()).hexdigest()
assert digest == response['snapshotFingerprint']
partitionRule、keys、claim、dateRange 与 lineage 都不参与身份计算;身份只回答“这组对象与这份契约是什么”,出处与覆盖声明由 lineage 与 claim 分别回答。
2. 分块下载
对 manifest.objects 中的每个文件循环调用 POST /v1/downloads:
{
"ticket": "<准备返回的 ticket>",
"objectSha256": "<文件的 sha256>",
"offset": 0,
"maxBytes": 1048576
}
响应体是该文件的一段字节(application/octet-stream),响应头给出续传信息:
| 响应头 | 说明 |
|---|---|
X-Buquant-Next-Offset | 下一段的起始偏移。 |
X-Buquant-Object-Bytes | 文件总字节数。 |
X-Buquant-Object-Sha256 | 文件的 SHA-256。 |
X-Buquant-Next-Offset 等于 X-Buquant-Object-Bytes 时该文件下载完成。拼接全部分段后计算 SHA-256,必须与 objectSha256 相同。
import hashlib
import requests
BASE_URL = 'https://api.<环境域名>' # 替换为实际服务地址,见快速开始
HEADERS = {'Authorization': 'Bearer <ACCESS_TOKEN>'}
def download(ticket, item, chunk_size):
chunks, offset = [], 0
while offset < item['bytes']:
response = requests.post(f'{BASE_URL}/v1/downloads', headers=HEADERS, timeout=60, json={
'ticket': ticket, 'objectSha256': item['sha256'], 'offset': offset, 'maxBytes': chunk_size})
response.raise_for_status()
chunks.append(response.content)
offset = int(response.headers['X-Buquant-Next-Offset'])
content = b''.join(chunks)
if hashlib.sha256(content).hexdigest() != item['sha256']:
raise ValueError('下载内容与 objectSha256 不一致')
return content
失效与重试
每次下载都会重新核验凭据的当前权限与版本身份:
| 状态 | 错误码 | 含义与处理 |
|---|---|---|
| 403 | forbidden | 凭据权限与准备时不一致。 |
| 409 | release-unavailable、cursor-release-unavailable | 版本不再是当前版本或已不可用。重新准备。 |
| 422 | invalid-cursor | 票据无效、过期,或该版本的分区选择已变化。重新准备。 |
| 422 | invalid-query | 文件不在本次准备的清单中,或偏移、长度越界。 |