跳到主要内容

批量下载

需要一个发布版本的大段历史时,先准备一份 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

失效与重试​

每次下载都会重新核验凭据的当前权限与版本身份:

状态错误码含义与处理
403forbidden凭据权限与准备时不一致。
409release-unavailable、cursor-release-unavailable版本不再是当前版本或已不可用。重新准备。
422invalid-cursor票据无效、过期,或该版本的分区选择已变化。重新准备。
422invalid-query文件不在本次准备的清单中,或偏移、长度越界。