quantdb-sdk 0.4.7__tar.gz → 0.5.0__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.4.7 → quantdb-sdk-0.5.0}/CHANGELOG.md +70 -14
- {quantdb_sdk-0.4.7/quantdb_sdk.egg-info → quantdb-sdk-0.5.0}/PKG-INFO +3 -13
- {quantdb_sdk-0.4.7 → quantdb-sdk-0.5.0}/pyproject.toml +1 -1
- {quantdb_sdk-0.4.7 → quantdb-sdk-0.5.0}/quantdb_sdk/__init__.py +1 -1
- {quantdb_sdk-0.4.7 → quantdb-sdk-0.5.0}/quantdb_sdk/__main__.py +71 -14
- {quantdb_sdk-0.4.7 → quantdb-sdk-0.5.0}/quantdb_sdk/_utils.py +1 -0
- {quantdb_sdk-0.4.7 → quantdb-sdk-0.5.0}/quantdb_sdk/async_client.py +59 -1
- {quantdb_sdk-0.4.7 → quantdb-sdk-0.5.0}/quantdb_sdk/client.py +105 -24
- {quantdb_sdk-0.4.7 → quantdb-sdk-0.5.0}/quantdb_sdk/hub.py +62 -5
- {quantdb_sdk-0.4.7 → quantdb-sdk-0.5.0}/quantdb_sdk/layout.py +5 -0
- {quantdb_sdk-0.4.7 → quantdb-sdk-0.5.0/quantdb_sdk.egg-info}/PKG-INFO +3 -13
- quantdb-sdk-0.5.0/quantdb_sdk.egg-info/SOURCES.txt +21 -0
- quantdb_sdk-0.4.7/quantdb_sdk/examples/__init__.py +0 -9
- quantdb_sdk-0.4.7/quantdb_sdk/examples/demo_01_connect_basics.py +0 -55
- quantdb_sdk-0.4.7/quantdb_sdk/examples/demo_02_kline_v2_daily.py +0 -48
- quantdb_sdk-0.4.7/quantdb_sdk/examples/demo_03_sync_full_market.py +0 -50
- quantdb_sdk-0.4.7/quantdb_sdk/examples/demo_04_duckdb_warehouse.py +0 -46
- quantdb_sdk-0.4.7/quantdb_sdk/examples/demo_05_read_local_data.py +0 -68
- quantdb_sdk-0.4.7/quantdb_sdk/examples/demo_06_tick_and_factors.py +0 -45
- quantdb_sdk-0.4.7/quantdb_sdk/examples/demo_07_data_hub.py +0 -66
- quantdb_sdk-0.4.7/quantdb_sdk.egg-info/SOURCES.txt +0 -38
- quantdb_sdk-0.4.7/tests/test_async_client.py +0 -218
- quantdb_sdk-0.4.7/tests/test_client.py +0 -312
- quantdb_sdk-0.4.7/tests/test_data_hub.py +0 -351
- quantdb_sdk-0.4.7/tests/test_hub_models.py +0 -125
- quantdb_sdk-0.4.7/tests/test_layout.py +0 -101
- quantdb_sdk-0.4.7/tests/test_mcp_download.py +0 -134
- quantdb_sdk-0.4.7/tests/test_technical_indicators.py +0 -237
- quantdb_sdk-0.4.7/tests/test_update_pipeline.py +0 -44
- quantdb_sdk-0.4.7/tests/test_utils.py +0 -102
- {quantdb_sdk-0.4.7 → quantdb-sdk-0.5.0}/LICENSE +0 -0
- {quantdb_sdk-0.4.7 → quantdb-sdk-0.5.0}/MANIFEST.in +0 -0
- {quantdb_sdk-0.4.7 → quantdb-sdk-0.5.0}/README.md +0 -0
- {quantdb_sdk-0.4.7 → quantdb-sdk-0.5.0}/quantdb_sdk/errors.py +0 -0
- {quantdb_sdk-0.4.7 → quantdb-sdk-0.5.0}/quantdb_sdk/mcp_download.py +0 -0
- {quantdb_sdk-0.4.7 → quantdb-sdk-0.5.0}/quantdb_sdk/py.typed +0 -0
- {quantdb_sdk-0.4.7 → quantdb-sdk-0.5.0}/quantdb_sdk.egg-info/dependency_links.txt +0 -0
- {quantdb_sdk-0.4.7 → quantdb-sdk-0.5.0}/quantdb_sdk.egg-info/entry_points.txt +0 -0
- {quantdb_sdk-0.4.7 → quantdb-sdk-0.5.0}/quantdb_sdk.egg-info/requires.txt +0 -0
- {quantdb_sdk-0.4.7 → quantdb-sdk-0.5.0}/quantdb_sdk.egg-info/top_level.txt +0 -0
- {quantdb_sdk-0.4.7 → quantdb-sdk-0.5.0}/setup.cfg +0 -0
|
@@ -3,6 +3,62 @@
|
|
|
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
|
+
## \[Unreleased]
|
|
7
|
+
|
|
8
|
+
### Removed
|
|
9
|
+
|
|
10
|
+
- **移除演示脚本包 `quantdb_sdk.examples`**(demo_01 ~ demo_07,共 7 个脚本):不再随 wheel 发布。CLI 帮助与文档中的演示脚本指引已同步删除,改用 `quantdb layout` / `quantdb datasets` 自述目录规范与数据集目录。
|
|
11
|
+
|
|
12
|
+
## \[0.5.0] - 2026-10-31
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **字段字典接入(列级字段说明)**:数据集可以自带一份字段字典 JSON(随数据集放在 COS),SDK 现在能直接读到列级的「中文名 / 公式 / 释义 / 分组 / 来源 / 方向」。首批支持 `factor_dictionary`(V5 全因子宽表,571 列 = 2 键 + 6 列后复权 OHLCV + 563 因子)。
|
|
17
|
+
- `client.get_fields(sub_category, group=..., query=..., field=..., limit=...)` → 列明细 DataFrame;
|
|
18
|
+
- `client.get_field_groups(sub_category)` → 分组摘要(组名 + 字段数);
|
|
19
|
+
- `client.get_field_dictionary(...)` → 原始响应(含 `schema_version` / `total_columns` / `groups`);
|
|
20
|
+
- `client.data.fields(...)` / `client.data.field_groups(...)` 为 Hub 层封装;
|
|
21
|
+
- 异步端对应 `a_get_fields` / `a_get_field_groups` / `a_get_field_dictionary` 与 `a_fields` / `a_field_groups`;
|
|
22
|
+
- `client.data.info("factor_dictionary")` 现在额外返回 `field_count` 与 `field_groups`(没有字典的数据集不受影响,404 被静默忽略)。
|
|
23
|
+
- 新增 CLI:`quantdb fields factor_dictionary [--group/--query/--field/--limit/--json]`,不带过滤条件时打印分组摘要。
|
|
24
|
+
- 字典接口**不消耗下载流量**;摘要响应在客户端进程内缓存,重复调用不重复请求(也不额外占用 30/min 限流额度)。
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- `describe_dataset`(MCP)在有字典时额外返回 `field_groups` / `field_count`,并提示用 `describe_fields` 查看列明细。
|
|
29
|
+
|
|
30
|
+
## \[0.4.9] - 2026-10-31
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- **注册新数据集 `factor_dictionary`(第 6 大类,订阅级)**:V5 全因子宽表,571 列(8 个键与后复权 OHLCV + 563 个因子),COS 布局为 `6_ml_datasets/factor_dictionary/dt=YYYYMMDD/data.parquet`。此前该数据集在 SDK 侧完全未注册,`client.data.*` / `sync_dataset` / `mount_local_dataset` / CLI / MCP 全部拒绝。本次补齐三张表:
|
|
35
|
+
- `_utils.SYNC_DATASET_CATEGORIES` 加 `"factor_dictionary" -> "6"`;
|
|
36
|
+
- `hub._ENTRIES` 加一条注册表条目(名称/描述/`subscription`/关键词/`v2_daily_partition`/选择器);
|
|
37
|
+
- `layout.V2_DAILY_PARTITION` 加 `factor_dictionary`。
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- **`layout.V2_DAILY_PARTITION` 与 COS 实际布局漂移**:补入 `min1_kline`、`min5_kline`、`etf_kline`。经 COS 复核,四者均为 `{category}/{sub}/dt=YYYYMMDD/data.parquet`,此前被 `dataset_layout()` 误判为 `v1_symbol`,导致公开 API `dataset_layout()` / `local_relative_path()` 与 `quantdb layout` 输出错误路径(etf_kline 的 V1 逐股票文件在 COS 上早已删除)。
|
|
42
|
+
- `hub` 模块 docstring 与 `DATASET_REGISTRY` 注释中的数据集数量(29)更正为 31;`__init__.__version__` 的 importlib 回退值(0.4.5)同步到 0.4.9。
|
|
43
|
+
|
|
44
|
+
### Known issues
|
|
45
|
+
|
|
46
|
+
- `tick_data` 在 COS 上同样是 `dt=YYYYMMDD/data.parquet`(单日全市场约 189 MB),但 SDK 仍按 `tick_shard` 处理本地读取(`{SYM}_{YYYYMMDD}.parquet` glob + 文件名取日期)。带 `symbol` 的 `read_local("tick_data", ...)` 因此找不到 dt 分区,属既有缺陷,切换布局是行为变更,留待单独修复。
|
|
47
|
+
|
|
48
|
+
## \[0.4.8] - 2026-10-27
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
|
|
52
|
+
- **`quantdb usage` 在中文 Windows 控制台崩溃**:`余额` 一行输出的是窄人民币符号 U+00A5,而 Windows 中文控制台默认编码是 GBK/cp936,**GBK 中不存在该字符**(GBK 用 0x80 表示),`print` 抛 `UnicodeEncodeError: 'gbk' codec can't encode character '\xa5'`,命令以异常收场、后续输出(订阅状态等)全部丢失。
|
|
53
|
+
修复两处:① 金额改用**全角人民币符号 U+FFE5**(GBK 可编码,中文终端显示正常);② CLI 入口新增 `_configure_stdout()`,把 stdout/stderr 的编码错误处理降级为 `replace`——以后任何不可编码字符只会显示为 `?`,不再中断命令。
|
|
54
|
+
|
|
55
|
+
### Added
|
|
56
|
+
|
|
57
|
+
- 新增回归测试 `tests/test_cli_encoding.py`(仓库根 `tests/`,不进 wheel):
|
|
58
|
+
- 扫描 SDK 全部源码的**字符串字面量**,断言每个字符都能被 GBK 编码——防止再次引入不可编码字符;
|
|
59
|
+
- 在 GBK 编码的 stdout 下渲染用量输出,断言不抛异常;
|
|
60
|
+
- 断言 `_configure_stdout()` 确实把编码错误处理改为 `replace`。
|
|
61
|
+
|
|
6
62
|
## \[0.4.7] - 2026-10-27
|
|
7
63
|
|
|
8
64
|
### Fixed
|
|
@@ -17,20 +73,20 @@
|
|
|
17
73
|
|
|
18
74
|
> 兼容性:0.4.7 对接 2026-10-27 上线的服务端版本;旧服务端(两跳 cdn-bridge)仍可正常同步,无需回退。
|
|
19
75
|
|
|
20
|
-
## \[0.4.6] - 2026-10-27
|
|
21
|
-
|
|
22
|
-
### Fixed
|
|
23
|
-
|
|
24
|
-
- **增量同步的「检查更新」不再重复计费**:`sync_dataset` 此前对本地已存在的文件先发一次带 `If-None-Match` 的 GET 探测版本。ETag 命中时服务端在预扣前返回 304(免流量),但**ETag 不匹配时(对象已更新,正是需要下载的常见情况)该请求会继续走到流量预扣 + CDN 直连,把整个文件传完**,客户端却丢弃响应体再重下一次。CDN 直连模式签发即计费、不退款,于是「检查更新」被白白计费一次。
|
|
25
|
-
|
|
26
|
-
实测近 2 天生产数据:下载请求 309,973 次但只涉及 71,675 个不同文件(**放大到 4.3 倍**),其中 **93.6% 是同 ETag 的纯重复下载**,纯浪费带宽与用户流量。
|
|
27
|
-
|
|
28
|
-
修复:探测改用服务端新增的 `HEAD /api/v1/data/download`——只回 ETag 与长度,在流量预扣之前返回,**零流量、零令牌、零 PG 事务**。ETag 命中时同样返回 304。同步与异步客户端的 V2 release 与 V1 manifest 两条探测路径均已改用 HEAD。
|
|
29
|
-
|
|
30
|
-
客户端侧行为不变:探测结果与本地记录一致即免流量跳过,不一致才真正下载。
|
|
31
|
-
|
|
32
|
-
兼容性:服务端不支持 HEAD 时(老版本返回 405)自动退回原 GET 流程,不影响可用性。
|
|
33
|
-
|
|
76
|
+
## \[0.4.6] - 2026-10-27
|
|
77
|
+
|
|
78
|
+
### Fixed
|
|
79
|
+
|
|
80
|
+
- **增量同步的「检查更新」不再重复计费**:`sync_dataset` 此前对本地已存在的文件先发一次带 `If-None-Match` 的 GET 探测版本。ETag 命中时服务端在预扣前返回 304(免流量),但**ETag 不匹配时(对象已更新,正是需要下载的常见情况)该请求会继续走到流量预扣 + CDN 直连,把整个文件传完**,客户端却丢弃响应体再重下一次。CDN 直连模式签发即计费、不退款,于是「检查更新」被白白计费一次。
|
|
81
|
+
|
|
82
|
+
实测近 2 天生产数据:下载请求 309,973 次但只涉及 71,675 个不同文件(**放大到 4.3 倍**),其中 **93.6% 是同 ETag 的纯重复下载**,纯浪费带宽与用户流量。
|
|
83
|
+
|
|
84
|
+
修复:探测改用服务端新增的 `HEAD /api/v1/data/download`——只回 ETag 与长度,在流量预扣之前返回,**零流量、零令牌、零 PG 事务**。ETag 命中时同样返回 304。同步与异步客户端的 V2 release 与 V1 manifest 两条探测路径均已改用 HEAD。
|
|
85
|
+
|
|
86
|
+
客户端侧行为不变:探测结果与本地记录一致即免流量跳过,不一致才真正下载。
|
|
87
|
+
|
|
88
|
+
兼容性:服务端不支持 HEAD 时(老版本返回 405)自动退回原 GET 流程,不影响可用性。
|
|
89
|
+
|
|
34
90
|
## \[0.4.5] - 2026-10-18
|
|
35
91
|
|
|
36
92
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: quantdb-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: QuantDB 量化数据平台官方 Python SDK
|
|
5
5
|
Author: QuantDB Team
|
|
6
6
|
License: MIT
|
|
@@ -22,18 +22,8 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
22
22
|
Classifier: Topic :: Office/Business :: Financial
|
|
23
23
|
Requires-Python: >=3.8
|
|
24
24
|
Description-Content-Type: text/markdown
|
|
25
|
-
License-File: LICENSE
|
|
26
|
-
Requires-Dist: requests>=2.25.0
|
|
27
|
-
Requires-Dist: pandas>=1.3.0
|
|
28
|
-
Requires-Dist: httpx>=0.24.0
|
|
29
|
-
Requires-Dist: duckdb>=1.0.0
|
|
30
|
-
Requires-Dist: pyarrow>=10.0.0
|
|
31
25
|
Provides-Extra: dev
|
|
32
|
-
|
|
33
|
-
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
34
|
-
Requires-Dist: responses>=0.23.0; extra == "dev"
|
|
35
|
-
Requires-Dist: respx>=0.20.0; extra == "dev"
|
|
36
|
-
Dynamic: license-file
|
|
26
|
+
License-File: LICENSE
|
|
37
27
|
|
|
38
28
|
# QuantDB Python SDK
|
|
39
29
|
|
|
@@ -7,9 +7,6 @@
|
|
|
7
7
|
python -m quantdb datasets # 列出全部数据集目录(无需 API Key)
|
|
8
8
|
python -m quantdb datasets l1_factors # 查看某个数据集详情
|
|
9
9
|
QUANTDB_API_KEY=qdb_xxx quantdb check # 验证 API Key 是否有效
|
|
10
|
-
|
|
11
|
-
演示脚本见 quantdb_sdk.examples(demo_01 ~ demo_06),
|
|
12
|
-
如: python -m quantdb_sdk.examples.demo_03_sync_full_market
|
|
13
10
|
"""
|
|
14
11
|
|
|
15
12
|
import argparse
|
|
@@ -63,11 +60,46 @@ def cmd_datasets(name: Optional[str] = None) -> int:
|
|
|
63
60
|
return 0
|
|
64
61
|
|
|
65
62
|
|
|
63
|
+
def _configure_stdout() -> None:
|
|
64
|
+
"""让 CLI 在非 UTF-8 控制台(中文 Windows 默认 GBK/cp936)下不因不可编码字符崩溃。
|
|
65
|
+
|
|
66
|
+
Windows 中文控制台的默认编码是 GBK,而窄人民币符号 U+00A5 在 GBK 中**不存在**
|
|
67
|
+
(GBK 用 0x80 表示该符号),直接 print 会抛
|
|
68
|
+
``UnicodeEncodeError: 'gbk' codec can't encode character '\\xa5'``,
|
|
69
|
+
导致整条命令以异常收场、后续输出全部丢失。
|
|
70
|
+
|
|
71
|
+
这里把 stdout/stderr 的编码错误处理改成 replace:无法编码的字符降级为 '?',
|
|
72
|
+
命令本身不再中断。配合正文中改用全角 ¥(U+FFE5,GBK 可编码),
|
|
73
|
+
中文 Windows 终端可正常显示金额。
|
|
74
|
+
"""
|
|
75
|
+
for stream in (sys.stdout, sys.stderr):
|
|
76
|
+
try:
|
|
77
|
+
stream.reconfigure(errors="replace") # type: ignore[attr-defined]
|
|
78
|
+
except Exception: # pragma: no cover - 老版本/非 TextIOWrapper 忽略
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def print_usage(usage: dict) -> None:
|
|
83
|
+
"""打印账户用量(抽成独立函数便于单测覆盖非 UTF-8 控制台场景)。"""
|
|
84
|
+
print("账户用量:")
|
|
85
|
+
print(f" 套餐: 已用 {usage['plan_used_gb']:.2f} GB / {usage['plan_total_gb']:.2f} GB"
|
|
86
|
+
f"(剩余 {usage['plan_remaining_gb']:.2f} GB)")
|
|
87
|
+
print(f" 流量包: 已用 {usage['package_used_gb']:.2f} GB / {usage['package_total_gb']:.2f} GB"
|
|
88
|
+
f"(剩余 {usage['package_remaining_gb']:.2f} GB)")
|
|
89
|
+
print(f" 合计: 已用 {usage['used_gb']:.2f} GB / {usage['total_gb']:.2f} GB"
|
|
90
|
+
f"(剩余 {usage['remaining_gb']:.2f} GB)")
|
|
91
|
+
# 全角人民币符号 U+FFE5:GBK 可编码;窄符号 U+00A5 在 GBK 中不存在会崩溃
|
|
92
|
+
print(f" 余额: ¥{usage['balance_yuan']:.2f}")
|
|
93
|
+
sub = usage.get("subscription", {})
|
|
94
|
+
print(f" 订阅: {sub.get('status', 'none')}")
|
|
95
|
+
|
|
96
|
+
|
|
66
97
|
def main() -> int:
|
|
98
|
+
_configure_stdout()
|
|
67
99
|
parser = argparse.ArgumentParser(
|
|
68
100
|
prog="quantdb",
|
|
69
101
|
description="QuantDB 量化数据平台官方 Python SDK",
|
|
70
|
-
epilog="
|
|
102
|
+
epilog="目录规范: quantdb layout;数据集目录: quantdb datasets",
|
|
71
103
|
)
|
|
72
104
|
parser.add_argument(
|
|
73
105
|
"--version",
|
|
@@ -115,6 +147,14 @@ def main() -> int:
|
|
|
115
147
|
download_parser.add_argument("--save-dir", default=None, help="本地数据根目录")
|
|
116
148
|
download_parser.add_argument("--json", action="store_true", help="输出 JSON 保存路径和文件大小")
|
|
117
149
|
|
|
150
|
+
fields_parser = subparsers.add_parser("fields", help="查询数据集的字段字典(列级说明,不消耗流量)")
|
|
151
|
+
fields_parser.add_argument("dataset", help="数据集,如 factor_dictionary")
|
|
152
|
+
fields_parser.add_argument("--group", default=None, help="分组子串,如 动量 / L2 2.1")
|
|
153
|
+
fields_parser.add_argument("--query", default=None, help="在字段名/中文名/释义/公式上子串匹配")
|
|
154
|
+
fields_parser.add_argument("--field", default=None, help="精确字段名,如 turn_1")
|
|
155
|
+
fields_parser.add_argument("--limit", type=int, default=None, help="返回列数上限(默认 200)")
|
|
156
|
+
fields_parser.add_argument("--json", action="store_true", help="输出 JSON")
|
|
157
|
+
|
|
118
158
|
args = parser.parse_args()
|
|
119
159
|
|
|
120
160
|
if not args.command:
|
|
@@ -158,16 +198,7 @@ def main() -> int:
|
|
|
158
198
|
|
|
159
199
|
if args.command == "usage":
|
|
160
200
|
usage = client.get_usage()
|
|
161
|
-
|
|
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')}")
|
|
201
|
+
print_usage(usage)
|
|
171
202
|
return 0
|
|
172
203
|
|
|
173
204
|
if args.command == "sync":
|
|
@@ -179,6 +210,32 @@ def main() -> int:
|
|
|
179
210
|
print(f" Release: {result['release_id']}")
|
|
180
211
|
return 0
|
|
181
212
|
|
|
213
|
+
if args.command == "fields":
|
|
214
|
+
filtered = bool(args.group or args.query or args.field)
|
|
215
|
+
if args.json:
|
|
216
|
+
payload = client.get_field_dictionary(
|
|
217
|
+
args.dataset, group=args.group, query=args.query,
|
|
218
|
+
field=args.field, limit=args.limit,
|
|
219
|
+
)
|
|
220
|
+
print(json.dumps(payload, ensure_ascii=False))
|
|
221
|
+
return 0
|
|
222
|
+
if not filtered:
|
|
223
|
+
groups = client.get_field_groups(args.dataset)
|
|
224
|
+
print(f"{args.dataset} 字段分组(共 {int(groups['count'].sum())} 列):")
|
|
225
|
+
for _, row in groups.iterrows():
|
|
226
|
+
print(f" {int(row['count']):>4} {row['group']}")
|
|
227
|
+
print("\n加 --group / --query / --field 查看列明细。")
|
|
228
|
+
return 0
|
|
229
|
+
df = client.get_fields(args.dataset, group=args.group, query=args.query,
|
|
230
|
+
field=args.field, limit=args.limit)
|
|
231
|
+
if df.empty:
|
|
232
|
+
print("没有匹配的字段。")
|
|
233
|
+
return 0
|
|
234
|
+
print(f"匹配 {len(df)} 列:")
|
|
235
|
+
for _, row in df.iterrows():
|
|
236
|
+
print(f" {row['field']:<32} {row['name_cn']:<18} [{row['group']}]")
|
|
237
|
+
return 0
|
|
238
|
+
|
|
182
239
|
except Exception as e:
|
|
183
240
|
print(f"[ERROR] {e}", file=sys.stderr)
|
|
184
241
|
return 1
|
|
@@ -23,6 +23,7 @@ SYNC_DATASET_CATEGORIES: Dict[str, str] = {
|
|
|
23
23
|
"etf_pcf": "4", "convertible_bond": "4", "etf_kline": "4",
|
|
24
24
|
"valuation": "5", "technical_indicators": "5", "market_sentiment": "5",
|
|
25
25
|
"features_daily": "6", "l1_factors": "6", "l1_l2_factors": "6", "l2_factors": "6",
|
|
26
|
+
"factor_dictionary": "6",
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
|
|
@@ -28,7 +28,7 @@ from .errors import (
|
|
|
28
28
|
)
|
|
29
29
|
|
|
30
30
|
# 维护提示:每次发版必须与 pyproject.toml 版本号同步
|
|
31
|
-
_USER_AGENT = "QuantDB-Python-SDK/0.
|
|
31
|
+
_USER_AGENT = "QuantDB-Python-SDK/0.5.0"
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
def _etag_hex(value: Optional[str]) -> Optional[str]:
|
|
@@ -83,6 +83,9 @@ class AsyncQuantDBClient:
|
|
|
83
83
|
# 进程内 parquet ETag 缓存:同对象(ETag 未变)不重复下载,避免重复计费。
|
|
84
84
|
self._cache: Dict[str, Dict[str, Any]] = {}
|
|
85
85
|
|
|
86
|
+
# 字段字典摘要缓存(dataset -> 原始响应),与同步客户端同语义。
|
|
87
|
+
self._field_cache: Dict[str, Dict[str, Any]] = {}
|
|
88
|
+
|
|
86
89
|
# 统一数据入口(quantdb_sdk.hub.AsyncDataHub)惰性实例,见 data property。
|
|
87
90
|
self._data_hub: Optional[Any] = None
|
|
88
91
|
|
|
@@ -609,6 +612,61 @@ class AsyncQuantDBClient:
|
|
|
609
612
|
df["file_size_gb"] = df["file_size"].fillna(0).map(bytes_to_gb).round(2)
|
|
610
613
|
return df
|
|
611
614
|
|
|
615
|
+
# ========== 字段字典(列级说明,不消耗下载流量) ==========
|
|
616
|
+
|
|
617
|
+
async def a_get_field_dictionary(
|
|
618
|
+
self,
|
|
619
|
+
sub_category: str,
|
|
620
|
+
group: Optional[str] = None,
|
|
621
|
+
query: Optional[str] = None,
|
|
622
|
+
field: Optional[str] = None,
|
|
623
|
+
limit: Optional[int] = None,
|
|
624
|
+
use_cache: bool = True,
|
|
625
|
+
) -> Dict[str, Any]:
|
|
626
|
+
"""返回字段字典原始响应(异步):schema 信息 + ``groups`` [+ ``columns``]。
|
|
627
|
+
|
|
628
|
+
不带过滤条件时服务端只返回分组摘要;同一进程内摘要会缓存复用。
|
|
629
|
+
"""
|
|
630
|
+
filtered = bool(group or query or field)
|
|
631
|
+
cache_key = f"summary:{sub_category}"
|
|
632
|
+
if use_cache and not filtered and cache_key in self._field_cache:
|
|
633
|
+
return self._field_cache[cache_key]
|
|
634
|
+
|
|
635
|
+
params: Dict[str, Any] = {"sub_category": sub_category}
|
|
636
|
+
if group:
|
|
637
|
+
params["group"] = group
|
|
638
|
+
if query:
|
|
639
|
+
params["query"] = query
|
|
640
|
+
if field:
|
|
641
|
+
params["field"] = field
|
|
642
|
+
if limit:
|
|
643
|
+
params["limit"] = int(limit)
|
|
644
|
+
payload = await self._get("/api/v1/data/fields", params)
|
|
645
|
+
if use_cache and not filtered:
|
|
646
|
+
self._field_cache[cache_key] = payload
|
|
647
|
+
return payload
|
|
648
|
+
|
|
649
|
+
async def a_get_fields(
|
|
650
|
+
self,
|
|
651
|
+
sub_category: str,
|
|
652
|
+
group: Optional[str] = None,
|
|
653
|
+
query: Optional[str] = None,
|
|
654
|
+
field: Optional[str] = None,
|
|
655
|
+
limit: Optional[int] = None,
|
|
656
|
+
) -> pd.DataFrame:
|
|
657
|
+
"""查询列级字段说明(异步);必须带 group / query / field 之一。"""
|
|
658
|
+
payload = await self.a_get_field_dictionary(
|
|
659
|
+
sub_category, group=group, query=query, field=field, limit=limit,
|
|
660
|
+
)
|
|
661
|
+
columns = payload.get("columns") or []
|
|
662
|
+
return pd.DataFrame(columns, columns=["field", "name_cn", "formula", "description",
|
|
663
|
+
"group", "source", "direction", "order"])
|
|
664
|
+
|
|
665
|
+
async def a_get_field_groups(self, sub_category: str) -> pd.DataFrame:
|
|
666
|
+
"""字段分组摘要(异步):``group`` + ``count``。"""
|
|
667
|
+
payload = await self.a_get_field_dictionary(sub_category)
|
|
668
|
+
return pd.DataFrame(payload.get("groups") or [], columns=["group", "count"])
|
|
669
|
+
|
|
612
670
|
# ========== 数据预览与下载 ==========
|
|
613
671
|
|
|
614
672
|
async def a_preview_as_df(
|
|
@@ -32,7 +32,13 @@ from .errors import (
|
|
|
32
32
|
)
|
|
33
33
|
|
|
34
34
|
# 维护提示:每次发版必须与 pyproject.toml 版本号同步
|
|
35
|
-
_USER_AGENT = "QuantDB-Python-SDK/0.
|
|
35
|
+
_USER_AGENT = "QuantDB-Python-SDK/0.5.0"
|
|
36
|
+
|
|
37
|
+
# 字段字典的列顺序(与后端 /api/v1/data/fields 返回的 columns[] 一致)
|
|
38
|
+
_FIELD_COLUMNS = (
|
|
39
|
+
"field", "name_cn", "formula", "description",
|
|
40
|
+
"group", "source", "direction", "order",
|
|
41
|
+
)
|
|
36
42
|
|
|
37
43
|
|
|
38
44
|
def _etag_hex(value: Optional[str]) -> Optional[str]:
|
|
@@ -95,6 +101,10 @@ class QuantDBClient:
|
|
|
95
101
|
# 只需下载一次。进程退出即失效(无跨进程副作用)。
|
|
96
102
|
self._cache: Dict[str, Dict[str, Any]] = {}
|
|
97
103
|
|
|
104
|
+
# 字段字典摘要缓存(dataset -> 原始响应):字典是静态文件,
|
|
105
|
+
# 同一进程内重复 info()/get_field_groups() 不必反复请求(也不吃 30/min 限流)。
|
|
106
|
+
self._field_cache: Dict[str, Dict[str, Any]] = {}
|
|
107
|
+
|
|
98
108
|
# 统一数据入口(quantdb_sdk.hub.DataHub)惰性实例,见 data property。
|
|
99
109
|
self._data_hub: Optional[Any] = None
|
|
100
110
|
|
|
@@ -145,29 +155,29 @@ class QuantDBClient:
|
|
|
145
155
|
resp = self._request("GET", path, params=params)
|
|
146
156
|
return self._check_response(resp)
|
|
147
157
|
|
|
148
|
-
def _probe_head(self, params: dict, headers: Optional[dict] = None) -> requests.Response:
|
|
149
|
-
"""HEAD 探测对象元数据(增量同步的「检查更新」):零流量、零令牌、零计费。
|
|
150
|
-
|
|
151
|
-
服务端在流量预扣之前返回 ETag 与 Content-Length,客户端与本地记录
|
|
152
|
-
比对后决定是否需要真正下载。
|
|
153
|
-
|
|
154
|
-
为什么必须用 HEAD 而不是带 If-None-Match 的 GET:
|
|
155
|
-
GET 探测在 ETag 不匹配时(对象已更新,正是需要下载的常见情况)会
|
|
156
|
-
继续走到流量预扣 + CDN 直连,把整个文件传完;客户端却丢弃响应体
|
|
157
|
-
再重下一次。CDN 直连模式签发即计费、不退款,于是「检查更新」被
|
|
158
|
-
白白计费一次——实测近 2 天下载请求量放大到文件数的 4.3 倍,
|
|
159
|
-
其中 93.6% 是同 ETag 的纯重复下载。
|
|
160
|
-
|
|
161
|
-
服务端不支持 HEAD 时(老版本)返回 405,调用方应退回 GET 流程。
|
|
162
|
-
"""
|
|
163
|
-
try:
|
|
164
|
-
return self._request(
|
|
165
|
-
"HEAD", "/api/v1/data/download", params=params,
|
|
166
|
-
headers=headers, allow_redirects=False,
|
|
167
|
-
)
|
|
168
|
-
except requests.RequestException:
|
|
169
|
-
return requests.Response()
|
|
170
|
-
|
|
158
|
+
def _probe_head(self, params: dict, headers: Optional[dict] = None) -> requests.Response:
|
|
159
|
+
"""HEAD 探测对象元数据(增量同步的「检查更新」):零流量、零令牌、零计费。
|
|
160
|
+
|
|
161
|
+
服务端在流量预扣之前返回 ETag 与 Content-Length,客户端与本地记录
|
|
162
|
+
比对后决定是否需要真正下载。
|
|
163
|
+
|
|
164
|
+
为什么必须用 HEAD 而不是带 If-None-Match 的 GET:
|
|
165
|
+
GET 探测在 ETag 不匹配时(对象已更新,正是需要下载的常见情况)会
|
|
166
|
+
继续走到流量预扣 + CDN 直连,把整个文件传完;客户端却丢弃响应体
|
|
167
|
+
再重下一次。CDN 直连模式签发即计费、不退款,于是「检查更新」被
|
|
168
|
+
白白计费一次——实测近 2 天下载请求量放大到文件数的 4.3 倍,
|
|
169
|
+
其中 93.6% 是同 ETag 的纯重复下载。
|
|
170
|
+
|
|
171
|
+
服务端不支持 HEAD 时(老版本)返回 405,调用方应退回 GET 流程。
|
|
172
|
+
"""
|
|
173
|
+
try:
|
|
174
|
+
return self._request(
|
|
175
|
+
"HEAD", "/api/v1/data/download", params=params,
|
|
176
|
+
headers=headers, allow_redirects=False,
|
|
177
|
+
)
|
|
178
|
+
except requests.RequestException:
|
|
179
|
+
return requests.Response()
|
|
180
|
+
|
|
171
181
|
def _download_stream(self, params: dict, headers: Optional[dict] = None) -> requests.Response:
|
|
172
182
|
"""下载专用流式请求:手动处理服务端 302 CDN 直连跳转。
|
|
173
183
|
|
|
@@ -684,6 +694,77 @@ class QuantDBClient:
|
|
|
684
694
|
df["file_size_gb"] = df["file_size"].fillna(0).map(bytes_to_gb).round(2)
|
|
685
695
|
return df
|
|
686
696
|
|
|
697
|
+
# ========== 字段字典(列级说明,不消耗下载流量) ==========
|
|
698
|
+
|
|
699
|
+
def get_field_dictionary(
|
|
700
|
+
self,
|
|
701
|
+
sub_category: str,
|
|
702
|
+
group: Optional[str] = None,
|
|
703
|
+
query: Optional[str] = None,
|
|
704
|
+
field: Optional[str] = None,
|
|
705
|
+
limit: Optional[int] = None,
|
|
706
|
+
use_cache: bool = True,
|
|
707
|
+
) -> Dict[str, Any]:
|
|
708
|
+
"""返回字段字典原始响应(schema 信息 + ``groups`` 分组摘要 [+ ``columns``])。
|
|
709
|
+
|
|
710
|
+
字典由数据侧随数据集发布(如 ``factor_dictionary`` 的
|
|
711
|
+
``v5_factor_dictionary.json``),后端读取 COS 后缓存,**不消耗下载流量**。
|
|
712
|
+
|
|
713
|
+
:param sub_category: 数据集标识,如 ``factor_dictionary``。
|
|
714
|
+
:param group: 可选,分组子串匹配(如 ``动量`` / ``L2 2.1``)。
|
|
715
|
+
:param query: 可选,在字段名/中文名/释义/公式上做子串匹配。
|
|
716
|
+
:param field: 可选,精确字段名(如 ``turn_1``)。
|
|
717
|
+
:param limit: 可选,返回列数上限(服务端默认 200,最大 1000)。
|
|
718
|
+
:param use_cache: 同一进程内复用已取过的「摘要」响应(仅当不带过滤条件时)。
|
|
719
|
+
:raises NotFoundError: 该数据集没有字段字典。
|
|
720
|
+
"""
|
|
721
|
+
filtered = bool(group or query or field)
|
|
722
|
+
cache_key = f"summary:{sub_category}"
|
|
723
|
+
if use_cache and not filtered and cache_key in self._field_cache:
|
|
724
|
+
return self._field_cache[cache_key]
|
|
725
|
+
|
|
726
|
+
params: Dict[str, Any] = {"sub_category": sub_category}
|
|
727
|
+
if group:
|
|
728
|
+
params["group"] = group
|
|
729
|
+
if query:
|
|
730
|
+
params["query"] = query
|
|
731
|
+
if field:
|
|
732
|
+
params["field"] = field
|
|
733
|
+
if limit:
|
|
734
|
+
params["limit"] = int(limit)
|
|
735
|
+
payload = self._get("/api/v1/data/fields", params)
|
|
736
|
+
if use_cache and not filtered:
|
|
737
|
+
self._field_cache[cache_key] = payload
|
|
738
|
+
return payload
|
|
739
|
+
|
|
740
|
+
def get_fields(
|
|
741
|
+
self,
|
|
742
|
+
sub_category: str,
|
|
743
|
+
group: Optional[str] = None,
|
|
744
|
+
query: Optional[str] = None,
|
|
745
|
+
field: Optional[str] = None,
|
|
746
|
+
limit: Optional[int] = None,
|
|
747
|
+
) -> pd.DataFrame:
|
|
748
|
+
"""查询某数据集的列级字段说明(中文名 / 公式 / 释义 / 分组 / 方向)。
|
|
749
|
+
|
|
750
|
+
**必须带 ``group`` / ``query`` / ``field`` 之一**:不带过滤条件时服务端只返回
|
|
751
|
+
分组摘要(571 列全量下发会撑爆调用方上下文),此时返回空 DataFrame,
|
|
752
|
+
请改用 :meth:`get_field_groups`。
|
|
753
|
+
|
|
754
|
+
:return: DataFrame,列为 ``field`` / ``name_cn`` / ``formula`` /
|
|
755
|
+
``description`` / ``group`` / ``source`` / ``direction`` / ``order``。
|
|
756
|
+
"""
|
|
757
|
+
payload = self.get_field_dictionary(
|
|
758
|
+
sub_category, group=group, query=query, field=field, limit=limit,
|
|
759
|
+
)
|
|
760
|
+
columns = payload.get("columns") or []
|
|
761
|
+
return pd.DataFrame(columns, columns=list(_FIELD_COLUMNS))
|
|
762
|
+
|
|
763
|
+
def get_field_groups(self, sub_category: str) -> pd.DataFrame:
|
|
764
|
+
"""字段分组摘要(``group`` 组名 + ``count`` 字段数),不消耗下载流量。"""
|
|
765
|
+
payload = self.get_field_dictionary(sub_category)
|
|
766
|
+
return pd.DataFrame(payload.get("groups") or [], columns=["group", "count"])
|
|
767
|
+
|
|
687
768
|
# ========== 数据预览与下载(消耗下载流量) ==========
|
|
688
769
|
|
|
689
770
|
def preview_as_df(
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
from quantdb_sdk import QuantDBClient
|
|
10
10
|
|
|
11
11
|
client = QuantDBClient(api_key="qdb_xxx")
|
|
12
|
-
client.data.datasets() #
|
|
12
|
+
client.data.datasets() # 31 个数据集目录(零流量)
|
|
13
13
|
client.data.info("l1_factors") # 覆盖区间 + 文件大小
|
|
14
14
|
client.data.read_local("daily_forward", start_date="2026-09-01") # 本地零流量
|
|
15
15
|
client.data.preview("balance", symbol="600519.SH") # 在线预览
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
``category_dir`` 复用 :data:`quantdb_sdk.layout.CATEGORY_DIRS`,避免重复维护分类事实源。
|
|
22
22
|
- ``layout`` 记录数据集的**实际 COS 形态**(``v2_daily_partition`` / ``v1_symbol`` /
|
|
23
23
|
``tick_shard``),用于 ``query()`` 路由;不用 :func:`quantdb_sdk.layout.dataset_layout`
|
|
24
|
-
(其 V2
|
|
24
|
+
(其 V2 集合截至 2026-10-31 已与 COS 对齐,但 tick_data 例外:COS 已改为 dt= 分区,
|
|
25
|
+
本地读取仍是分片语义)。
|
|
25
26
|
- 本地读取用**多形态 glob** 与目录布局集合解耦,支持显式 ``root``(数据根目录可配置,
|
|
26
27
|
非固定目录;缺省读 :func:`quantdb_sdk.layout.data_root`);读取本身交给 DuckDB 对
|
|
27
28
|
精确文件列表做一次 ``read_parquet``(列裁剪 / symbol / 日期下推),不逐文件拼接。
|
|
@@ -157,6 +158,8 @@ _ENTRIES: Tuple[Tuple[str, str, str, str, Tuple[str, ...], str, Tuple[str, ...]]
|
|
|
157
158
|
("l2", "因子", "factor"), "v2_daily_partition", ("symbol", "trade_date")),
|
|
158
159
|
("l1_l2_factors", "L1+L2 合并因子", "L1 和 L2 的合并宽表。", "subscription",
|
|
159
160
|
("l1", "l2", "因子", "factor", "合并"), "v2_daily_partition", ("symbol", "trade_date")),
|
|
161
|
+
("factor_dictionary", "V5 因子宽表", "V5 全因子宽表:571 列(2 键 + 6 列后复权 OHLCV + 563 因子),按交易日分区;字段字典见数据集目录内 v5_factor_dictionary.json。", "subscription",
|
|
162
|
+
("因子", "factor", "v5", "宽表", "f500", "字典"), "v2_daily_partition", ("symbol", "trade_date")),
|
|
160
163
|
)
|
|
161
164
|
|
|
162
165
|
|
|
@@ -178,7 +181,7 @@ def _build_registry() -> Dict[str, DatasetInfo]:
|
|
|
178
181
|
return registry
|
|
179
182
|
|
|
180
183
|
|
|
181
|
-
#: dataset 名 -> DatasetInfo(
|
|
184
|
+
#: dataset 名 -> DatasetInfo(31 项)
|
|
182
185
|
DATASET_REGISTRY: Dict[str, DatasetInfo] = _build_registry()
|
|
183
186
|
|
|
184
187
|
|
|
@@ -509,7 +512,9 @@ class DataHub:
|
|
|
509
512
|
"""返回数据集注册表信息,并可选叠加云端元数据(覆盖区间/文件数/大小)。
|
|
510
513
|
|
|
511
514
|
``with_meta=True``(默认)时叠加 ``client.query_meta(sub_category=dataset)``
|
|
512
|
-
的 ``min_date/max_date/file_count/file_size_gb/row_count/updated_at
|
|
515
|
+
的 ``min_date/max_date/file_count/file_size_gb/row_count/updated_at``,
|
|
516
|
+
并在该数据集带**字段字典**时额外叠加 ``field_count`` 与 ``field_groups``
|
|
517
|
+
(如 ``factor_dictionary`` 的 571 列分组)。没有字典的数据集不受影响。
|
|
513
518
|
"""
|
|
514
519
|
entry = _info(dataset).to_dict()
|
|
515
520
|
if not with_meta:
|
|
@@ -519,8 +524,36 @@ class DataHub:
|
|
|
519
524
|
except Exception:
|
|
520
525
|
meta = None
|
|
521
526
|
entry.update(_meta_row(meta))
|
|
527
|
+
try:
|
|
528
|
+
fields = self._client.get_field_dictionary(dataset)
|
|
529
|
+
except Exception:
|
|
530
|
+
fields = None
|
|
531
|
+
if isinstance(fields, dict) and fields.get("groups"):
|
|
532
|
+
entry["field_count"] = fields.get("total_columns")
|
|
533
|
+
entry["field_groups"] = fields.get("groups")
|
|
522
534
|
return entry
|
|
523
535
|
|
|
536
|
+
def fields(
|
|
537
|
+
self,
|
|
538
|
+
dataset: str,
|
|
539
|
+
group: Optional[str] = None,
|
|
540
|
+
query: Optional[str] = None,
|
|
541
|
+
field: Optional[str] = None,
|
|
542
|
+
limit: Optional[int] = None,
|
|
543
|
+
) -> pd.DataFrame:
|
|
544
|
+
"""查询列级字段说明(中文名 / 公式 / 释义 / 分组 / 方向),不消耗下载流量。
|
|
545
|
+
|
|
546
|
+
必须带 ``group`` / ``query`` / ``field`` 之一,否则服务端只回分组摘要、
|
|
547
|
+
返回空 DataFrame(用 :meth:`field_groups` 拿摘要)。
|
|
548
|
+
"""
|
|
549
|
+
return self._client.get_fields(
|
|
550
|
+
_info(dataset).dataset, group=group, query=query, field=field, limit=limit,
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
def field_groups(self, dataset: str) -> pd.DataFrame:
|
|
554
|
+
"""字段分组摘要(``group`` + ``count``),不消耗下载流量。"""
|
|
555
|
+
return self._client.get_field_groups(_info(dataset).dataset)
|
|
556
|
+
|
|
524
557
|
def local_files(
|
|
525
558
|
self,
|
|
526
559
|
dataset: str,
|
|
@@ -827,7 +860,7 @@ class AsyncDataHub:
|
|
|
827
860
|
return pd.DataFrame(rows, columns=cols)
|
|
828
861
|
|
|
829
862
|
async def a_info(self, dataset: str, with_meta: bool = True) -> Dict[str, Any]:
|
|
830
|
-
"""异步版 :meth:`DataHub.info
|
|
863
|
+
"""异步版 :meth:`DataHub.info`(含字段字典分组摘要)。"""
|
|
831
864
|
entry = _info(dataset).to_dict()
|
|
832
865
|
if not with_meta:
|
|
833
866
|
return entry
|
|
@@ -836,8 +869,32 @@ class AsyncDataHub:
|
|
|
836
869
|
except Exception:
|
|
837
870
|
meta = None
|
|
838
871
|
entry.update(_meta_row(meta))
|
|
872
|
+
try:
|
|
873
|
+
fields = await self._client.a_get_field_dictionary(dataset)
|
|
874
|
+
except Exception:
|
|
875
|
+
fields = None
|
|
876
|
+
if isinstance(fields, dict) and fields.get("groups"):
|
|
877
|
+
entry["field_count"] = fields.get("total_columns")
|
|
878
|
+
entry["field_groups"] = fields.get("groups")
|
|
839
879
|
return entry
|
|
840
880
|
|
|
881
|
+
async def a_fields(
|
|
882
|
+
self,
|
|
883
|
+
dataset: str,
|
|
884
|
+
group: Optional[str] = None,
|
|
885
|
+
query: Optional[str] = None,
|
|
886
|
+
field: Optional[str] = None,
|
|
887
|
+
limit: Optional[int] = None,
|
|
888
|
+
) -> pd.DataFrame:
|
|
889
|
+
"""异步版 :meth:`DataHub.fields`。"""
|
|
890
|
+
return await self._client.a_get_fields(
|
|
891
|
+
_info(dataset).dataset, group=group, query=query, field=field, limit=limit,
|
|
892
|
+
)
|
|
893
|
+
|
|
894
|
+
async def a_field_groups(self, dataset: str) -> pd.DataFrame:
|
|
895
|
+
"""异步版 :meth:`DataHub.field_groups`。"""
|
|
896
|
+
return await self._client.a_get_field_groups(_info(dataset).dataset)
|
|
897
|
+
|
|
841
898
|
async def a_local_files(
|
|
842
899
|
self,
|
|
843
900
|
dataset: str,
|