tinet-data-foundry-cli 0.1.6__tar.gz → 0.1.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.
Files changed (19) hide show
  1. {tinet_data_foundry_cli-0.1.6 → tinet_data_foundry_cli-0.1.9}/PKG-INFO +17 -6
  2. {tinet_data_foundry_cli-0.1.6 → tinet_data_foundry_cli-0.1.9}/README.md +16 -5
  3. {tinet_data_foundry_cli-0.1.6 → tinet_data_foundry_cli-0.1.9}/pyproject.toml +1 -1
  4. tinet_data_foundry_cli-0.1.9/src/tinet_data_foundry_cli/__init__.py +1 -0
  5. {tinet_data_foundry_cli-0.1.6 → tinet_data_foundry_cli-0.1.9}/src/tinet_data_foundry_cli/cli.py +61 -2
  6. {tinet_data_foundry_cli-0.1.6 → tinet_data_foundry_cli-0.1.9}/src/tinet_data_foundry_cli/docs.py +39 -10
  7. {tinet_data_foundry_cli-0.1.6 → tinet_data_foundry_cli-0.1.9}/src/tinet_data_foundry_cli.egg-info/PKG-INFO +17 -6
  8. {tinet_data_foundry_cli-0.1.6 → tinet_data_foundry_cli-0.1.9}/tests/test_cli.py +6 -0
  9. {tinet_data_foundry_cli-0.1.6 → tinet_data_foundry_cli-0.1.9}/tests/test_docs.py +27 -2
  10. tinet_data_foundry_cli-0.1.6/src/tinet_data_foundry_cli/__init__.py +0 -1
  11. {tinet_data_foundry_cli-0.1.6 → tinet_data_foundry_cli-0.1.9}/setup.cfg +0 -0
  12. {tinet_data_foundry_cli-0.1.6 → tinet_data_foundry_cli-0.1.9}/src/tinet_data_foundry_cli/__main__.py +0 -0
  13. {tinet_data_foundry_cli-0.1.6 → tinet_data_foundry_cli-0.1.9}/src/tinet_data_foundry_cli/auth.py +0 -0
  14. {tinet_data_foundry_cli-0.1.6 → tinet_data_foundry_cli-0.1.9}/src/tinet_data_foundry_cli/client.py +0 -0
  15. {tinet_data_foundry_cli-0.1.6 → tinet_data_foundry_cli-0.1.9}/src/tinet_data_foundry_cli.egg-info/SOURCES.txt +0 -0
  16. {tinet_data_foundry_cli-0.1.6 → tinet_data_foundry_cli-0.1.9}/src/tinet_data_foundry_cli.egg-info/dependency_links.txt +0 -0
  17. {tinet_data_foundry_cli-0.1.6 → tinet_data_foundry_cli-0.1.9}/src/tinet_data_foundry_cli.egg-info/entry_points.txt +0 -0
  18. {tinet_data_foundry_cli-0.1.6 → tinet_data_foundry_cli-0.1.9}/src/tinet_data_foundry_cli.egg-info/top_level.txt +0 -0
  19. {tinet_data_foundry_cli-0.1.6 → tinet_data_foundry_cli-0.1.9}/tests/test_sign.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tinet-data-foundry-cli
3
- Version: 0.1.6
3
+ Version: 0.1.9
4
4
  Summary: Tinet Data Foundry 取数 CLI(docs / catalog / query / schema)
5
5
  Author: Tinet
6
6
  License: MIT
@@ -57,6 +57,15 @@ tdf query \
57
57
 
58
58
  仅按日分区表必须带分区条件。未传分区字段时,服务端按上海时区补当天闭区间,并在响应 `data.partitionWindow` 里返回实际 `field/from/to`。显式 `between` 不超过 30 天,否则 422 `PARTITION_RANGE_INVALID`。按月、按年分区字段不是必填条件。
59
59
 
60
+ ODS 可以查询和过滤 `_source_ds`,值是来源数据源编码。编码清单:
61
+
62
+ ```bash
63
+ tdf datasource-codes
64
+ tdf datasource-codes --code nboss
65
+ ```
66
+
67
+ 只筛分区字段(如 `startTime`)时,服务端按同一范围另补 `_df_pt` 裁分区。其他时间字段不补。`_df_pt` 不能作为查询列。响应 `partitionWindow.prunedFrom` / `prunedTo` 是实际裁剪区间。
68
+
60
69
  若 pip 提示 scripts 目录不在 PATH,把该目录加入 PATH,或用 `python3 -m tinet_data_foundry_cli`。
61
70
 
62
71
  ## Windows(cmd.exe)
@@ -87,23 +96,25 @@ tdf catalog --insecure
87
96
  | `tdf docs rev` | `GET /docs/credentials/{ak}?format=rev` |
88
97
  | `tdf docs pull` | 按 `copy.catalog.policy` 只拉取变化的 `part=public` 或 `part=policy` |
89
98
  | `tdf catalog` | `GET /data-api/v1/catalog` |
99
+ | `tdf datasource-codes` | `GET /data-api/v1/datasource-codes` |
90
100
  | `tdf query` | `POST /data-api/v1/datasets/{id}/query` |
91
101
  | `tdf schema` | `GET /data-api/v1/datasets/{id}/schema` |
92
102
 
93
103
  文档站点默认 `https://data-foundry.tinetcloud.com`,可用 `DF_DOCS_BASE` 覆盖。`tdf docs` 只需要 `DF_AK`。缓存位于 `~/.cache/tinet-data-foundry/docs/<AK 哈希>/`,可用 `DF_DOCS_CACHE` 改根目录。stdout 非终端时 `tdf catalog` 改为 JSON。
94
104
 
95
- ### Skill 查询大目录
105
+ ### 业务 Skill 锁定数据集
96
106
 
97
- `tdf docs pull` 把公共目录与授权策略缓存到本机。Skill / Agent 不应直接把大 `public.json` 读进上下文,而是通过以下命令只取需要的精简结果:
107
+ `tdf docs pull` 把公共目录与授权策略缓存到本机。业务 Skill 自行检索和阅读字段,不要把 `public.json` 或 `policy.json` 读进上下文:
98
108
 
99
109
  ```bash
100
- tdf docs search --query "京东云消费"
110
+ tdf docs pull
111
+ tdf docs search --query "<检索词>"
101
112
  tdf docs describe --dataset "DATASET_ID"
102
113
  ```
103
114
 
104
- `search` 默认只返回有表权限的数据集;`describe` 只返回该表已授权字段及其脱敏规则。首次使用先执行 `tdf docs pull`。
115
+ `search` 默认只返回有表权限的数据集,表名或中文名精确匹配排在前面,最多 20 条。`describe` 只返回该表已授权字段、分区粒度和脱敏规则。需要与网关当前结构核对时再用 `tdf schema --dataset "DATASET_ID"`。不要用不带 `--keyword` 的 `tdf catalog` 拉取整份目录。
105
116
 
106
- Agent / Skill 传查询体用文件,避免 shell 转义:
117
+ DataFoundry 查询 Skill 只执行已经写好的查询。查询体用文件传入,避免 shell 转义:
107
118
 
108
119
  ```bash
109
120
  tdf query --dataset "DATASET_ID" --body-file query.json
@@ -40,6 +40,15 @@ tdf query \
40
40
 
41
41
  仅按日分区表必须带分区条件。未传分区字段时,服务端按上海时区补当天闭区间,并在响应 `data.partitionWindow` 里返回实际 `field/from/to`。显式 `between` 不超过 30 天,否则 422 `PARTITION_RANGE_INVALID`。按月、按年分区字段不是必填条件。
42
42
 
43
+ ODS 可以查询和过滤 `_source_ds`,值是来源数据源编码。编码清单:
44
+
45
+ ```bash
46
+ tdf datasource-codes
47
+ tdf datasource-codes --code nboss
48
+ ```
49
+
50
+ 只筛分区字段(如 `startTime`)时,服务端按同一范围另补 `_df_pt` 裁分区。其他时间字段不补。`_df_pt` 不能作为查询列。响应 `partitionWindow.prunedFrom` / `prunedTo` 是实际裁剪区间。
51
+
43
52
  若 pip 提示 scripts 目录不在 PATH,把该目录加入 PATH,或用 `python3 -m tinet_data_foundry_cli`。
44
53
 
45
54
  ## Windows(cmd.exe)
@@ -70,23 +79,25 @@ tdf catalog --insecure
70
79
  | `tdf docs rev` | `GET /docs/credentials/{ak}?format=rev` |
71
80
  | `tdf docs pull` | 按 `copy.catalog.policy` 只拉取变化的 `part=public` 或 `part=policy` |
72
81
  | `tdf catalog` | `GET /data-api/v1/catalog` |
82
+ | `tdf datasource-codes` | `GET /data-api/v1/datasource-codes` |
73
83
  | `tdf query` | `POST /data-api/v1/datasets/{id}/query` |
74
84
  | `tdf schema` | `GET /data-api/v1/datasets/{id}/schema` |
75
85
 
76
86
  文档站点默认 `https://data-foundry.tinetcloud.com`,可用 `DF_DOCS_BASE` 覆盖。`tdf docs` 只需要 `DF_AK`。缓存位于 `~/.cache/tinet-data-foundry/docs/<AK 哈希>/`,可用 `DF_DOCS_CACHE` 改根目录。stdout 非终端时 `tdf catalog` 改为 JSON。
77
87
 
78
- ### Skill 查询大目录
88
+ ### 业务 Skill 锁定数据集
79
89
 
80
- `tdf docs pull` 把公共目录与授权策略缓存到本机。Skill / Agent 不应直接把大 `public.json` 读进上下文,而是通过以下命令只取需要的精简结果:
90
+ `tdf docs pull` 把公共目录与授权策略缓存到本机。业务 Skill 自行检索和阅读字段,不要把 `public.json` 或 `policy.json` 读进上下文:
81
91
 
82
92
  ```bash
83
- tdf docs search --query "京东云消费"
93
+ tdf docs pull
94
+ tdf docs search --query "<检索词>"
84
95
  tdf docs describe --dataset "DATASET_ID"
85
96
  ```
86
97
 
87
- `search` 默认只返回有表权限的数据集;`describe` 只返回该表已授权字段及其脱敏规则。首次使用先执行 `tdf docs pull`。
98
+ `search` 默认只返回有表权限的数据集,表名或中文名精确匹配排在前面,最多 20 条。`describe` 只返回该表已授权字段、分区粒度和脱敏规则。需要与网关当前结构核对时再用 `tdf schema --dataset "DATASET_ID"`。不要用不带 `--keyword` 的 `tdf catalog` 拉取整份目录。
88
99
 
89
- Agent / Skill 传查询体用文件,避免 shell 转义:
100
+ DataFoundry 查询 Skill 只执行已经写好的查询。查询体用文件传入,避免 shell 转义:
90
101
 
91
102
  ```bash
92
103
  tdf query --dataset "DATASET_ID" --body-file query.json
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tinet-data-foundry-cli"
7
- version = "0.1.6"
7
+ version = "0.1.9"
8
8
  description = "Tinet Data Foundry 取数 CLI(docs / catalog / query / schema)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -0,0 +1 @@
1
+ __version__ = "0.1.9"
@@ -82,6 +82,56 @@ def _ok_data(parsed: Any) -> Any:
82
82
  return parsed.get("data") if isinstance(parsed, dict) else parsed
83
83
 
84
84
 
85
+ def _print_datasource_codes(rows: List[Mapping[str, Any]]) -> None:
86
+ if not rows:
87
+ print("当前凭证没有可查询的数据源编码。")
88
+ return
89
+ cols = [
90
+ ("code", "编码", 24),
91
+ ("name", "名称", 28),
92
+ ("id", "id", 36),
93
+ ]
94
+ header = " ".join(_cell(title, w) for _, title, w in cols)
95
+ print(header)
96
+ print("-" * len(header))
97
+ for row in rows:
98
+ print(" ".join(_cell(row.get(key), w) for key, _, w in cols))
99
+
100
+
101
+ def cmd_datasource_codes(args: argparse.Namespace) -> int:
102
+ base, ak, sk = credentials(args.endpoint)
103
+ query = {
104
+ "code": args.code or "",
105
+ "keyword": args.keyword or "",
106
+ }
107
+ try:
108
+ _, parsed, _ = request(
109
+ "GET",
110
+ base,
111
+ "/data-api/v1/datasource-codes",
112
+ ak,
113
+ sk,
114
+ query=query,
115
+ timeout=args.timeout,
116
+ insecure=args.insecure,
117
+ dry_run=args.dry_run,
118
+ )
119
+ except ApiError as ex:
120
+ _exit_api(ex)
121
+ except TransportError as ex:
122
+ _exit_transport(ex)
123
+ fmt = args.format
124
+ if fmt == "table" and not sys.stdout.isatty():
125
+ fmt = "json"
126
+ if args.dry_run or fmt == "json":
127
+ _print_json(parsed)
128
+ return 0
129
+ data = _ok_data(parsed) or {}
130
+ rows = data.get("list") if isinstance(data, dict) else []
131
+ _print_datasource_codes(list(rows or []))
132
+ return 0
133
+
134
+
85
135
  def cmd_catalog(args: argparse.Namespace) -> int:
86
136
  base, ak, sk = credentials(args.endpoint)
87
137
  query = {
@@ -249,6 +299,13 @@ def build_parser() -> argparse.ArgumentParser:
249
299
  c.add_argument("--format", choices=["table", "json"], default="table")
250
300
  c.set_defaults(func=cmd_catalog)
251
301
 
302
+ codes = sub.add_parser("datasource-codes", help="列出当前凭证可查询数据集关联的数据源编码")
303
+ _add_insecure(codes, overlay=True)
304
+ codes.add_argument("--code", help="按编码精确匹配,忽略大小写")
305
+ codes.add_argument("--keyword", help="按编码或名称包含匹配")
306
+ codes.add_argument("--format", choices=["table", "json"], default="table")
307
+ codes.set_defaults(func=cmd_datasource_codes)
308
+
252
309
  q = sub.add_parser("query", help="查询 Dataset")
253
310
  _add_insecure(q, overlay=True)
254
311
  q.add_argument("--dataset", required=True)
@@ -260,7 +317,8 @@ def build_parser() -> argparse.ArgumentParser:
260
317
  help=(
261
318
  "filter AST JSON。字符串字段可 starts_with(右模糊)、ends_with(左模糊)、contains(左右模糊)。"
262
319
  "仅按日分区必填:省略分区字段时按上海时区补当天,显式 between 不超过 30 天。"
263
- "按月、按年分区字段不是必填条件"
320
+ "按月、按年分区字段不是必填条件。"
321
+ "只筛分区字段时,服务端另补 _df_pt 裁分区;其他时间字段不补。_df_pt 不能作为查询列"
264
322
  ),
265
323
  )
266
324
  q.add_argument("--sort", help="sort JSON 数组")
@@ -280,9 +338,10 @@ def build_parser() -> argparse.ArgumentParser:
280
338
  d = sub.add_parser("docs", help="核对数据集开发文档修订号,并在本地缓存中检索数据集")
281
339
  _add_insecure(d, overlay=True)
282
340
  d.add_argument("action", choices=["rev", "pull", "search", "describe"])
283
- d.add_argument("--query", help="search:按名称、描述、表名或业务域检索本地缓存")
341
+ d.add_argument("--query", help="search:按名称、描述、表名或业务域检索本地缓存。表名精确匹配优先")
284
342
  d.add_argument("--dataset", help="describe:输出指定数据集的已授权字段")
285
343
  d.add_argument("--all", action="store_true", help="search:包含无表权限的数据集")
344
+ d.add_argument("--limit", type=int, default=20, help="search:最多返回条数,默认 20")
286
345
  d.add_argument("--docs-base", help="覆盖文档站点。默认 DF_DOCS_BASE 或 https://data-foundry.tinetcloud.com")
287
346
  d.set_defaults(func=cmd_docs)
288
347
 
@@ -48,7 +48,12 @@ def run_docs(args: Any) -> int:
48
48
  base = docs_base(getattr(args, "docs_base", None))
49
49
  folder = cache_dir(ak)
50
50
  if args.action == "search":
51
- return _search(folder, str(getattr(args, "query", "") or ""), bool(getattr(args, "all", False)))
51
+ return _search(
52
+ folder,
53
+ str(getattr(args, "query", "") or ""),
54
+ bool(getattr(args, "all", False)),
55
+ int(getattr(args, "limit", None) or 20),
56
+ )
52
57
  if args.action == "describe":
53
58
  return _describe(folder, str(getattr(args, "dataset", "") or ""))
54
59
  url = docs_url(base, ak, "rev")
@@ -76,25 +81,25 @@ def _cached_docs(folder: Path) -> tuple[Dict[str, Any], Dict[str, Any]]:
76
81
  return public, policy
77
82
 
78
83
 
79
- def _search(folder: Path, query: str, include_denied: bool) -> int:
84
+ def _search(folder: Path, query: str, include_denied: bool, limit: int = 20) -> int:
80
85
  public, policy = _cached_docs(folder)
81
86
  needle = query.strip().casefold()
82
87
  if not needle:
83
88
  raise SystemExit("docs search 需要 --query")
89
+ if limit < 1:
90
+ raise SystemExit("docs search --limit 必须大于 0")
84
91
  grants = _index(policy)
85
- matches: List[Dict[str, Any]] = []
92
+ ranked: List[tuple] = []
86
93
  for dataset in public.get("datasets") or []:
87
94
  if not isinstance(dataset, dict):
88
95
  continue
89
96
  grant = grants.get(str(dataset.get("id") or ""), {})
90
97
  if not include_denied and not grant.get("tableGranted"):
91
98
  continue
92
- haystack = " ".join(str(dataset.get(key) or "") for key in (
93
- "id", "name", "chineseName", "tableComment", "description", "businessDomainName",
94
- )).casefold()
95
- if needle not in haystack:
99
+ rank = _match_rank(dataset, needle)
100
+ if rank is None:
96
101
  continue
97
- matches.append({
102
+ ranked.append((rank, str(dataset.get("name") or ""), {
98
103
  "id": dataset.get("id"),
99
104
  "name": dataset.get("name"),
100
105
  "chineseName": dataset.get("chineseName"),
@@ -103,11 +108,34 @@ def _search(folder: Path, query: str, include_denied: bool) -> int:
103
108
  "layer": dataset.get("layer"),
104
109
  "schemaVersion": dataset.get("schemaVersion"),
105
110
  "tableGranted": bool(grant.get("tableGranted")),
106
- })
107
- _print({"query": query, "datasets": matches})
111
+ }))
112
+ ranked.sort(key=lambda row: (row[0], row[1]))
113
+ shown = ranked[:limit]
114
+ _print({
115
+ "query": query,
116
+ "total": len(ranked),
117
+ "truncated": len(ranked) > limit,
118
+ "datasets": [row[2] for row in shown],
119
+ })
108
120
  return 0
109
121
 
110
122
 
123
+ def _match_rank(dataset: Dict[str, Any], needle: str) -> Optional[int]:
124
+ identity = [str(dataset.get(key) or "").casefold() for key in ("id", "name", "chineseName")]
125
+ if needle in identity:
126
+ return 0
127
+ if any(value.startswith(needle) for value in identity if value):
128
+ return 1
129
+ if any(needle in value for value in identity if value):
130
+ return 2
131
+ extra = " ".join(str(dataset.get(key) or "") for key in (
132
+ "tableComment", "description", "businessDomainName",
133
+ )).casefold()
134
+ if needle in extra:
135
+ return 3
136
+ return None
137
+
138
+
111
139
  def _describe(folder: Path, dataset_id: str) -> int:
112
140
  public, policy = _cached_docs(folder)
113
141
  if not dataset_id.strip():
@@ -135,6 +163,7 @@ def _describe(folder: Path, dataset_id: str) -> int:
135
163
  "partition": column.get("partition"),
136
164
  "partitionGrain": column.get("partitionGrain"),
137
165
  "partitionRequired": column.get("partitionRequired"),
166
+ "suggestedRange": column.get("suggestedRange"),
138
167
  "outputRule": rules[name].get("outputRule"),
139
168
  })
140
169
  _print({
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tinet-data-foundry-cli
3
- Version: 0.1.6
3
+ Version: 0.1.9
4
4
  Summary: Tinet Data Foundry 取数 CLI(docs / catalog / query / schema)
5
5
  Author: Tinet
6
6
  License: MIT
@@ -57,6 +57,15 @@ tdf query \
57
57
 
58
58
  仅按日分区表必须带分区条件。未传分区字段时,服务端按上海时区补当天闭区间,并在响应 `data.partitionWindow` 里返回实际 `field/from/to`。显式 `between` 不超过 30 天,否则 422 `PARTITION_RANGE_INVALID`。按月、按年分区字段不是必填条件。
59
59
 
60
+ ODS 可以查询和过滤 `_source_ds`,值是来源数据源编码。编码清单:
61
+
62
+ ```bash
63
+ tdf datasource-codes
64
+ tdf datasource-codes --code nboss
65
+ ```
66
+
67
+ 只筛分区字段(如 `startTime`)时,服务端按同一范围另补 `_df_pt` 裁分区。其他时间字段不补。`_df_pt` 不能作为查询列。响应 `partitionWindow.prunedFrom` / `prunedTo` 是实际裁剪区间。
68
+
60
69
  若 pip 提示 scripts 目录不在 PATH,把该目录加入 PATH,或用 `python3 -m tinet_data_foundry_cli`。
61
70
 
62
71
  ## Windows(cmd.exe)
@@ -87,23 +96,25 @@ tdf catalog --insecure
87
96
  | `tdf docs rev` | `GET /docs/credentials/{ak}?format=rev` |
88
97
  | `tdf docs pull` | 按 `copy.catalog.policy` 只拉取变化的 `part=public` 或 `part=policy` |
89
98
  | `tdf catalog` | `GET /data-api/v1/catalog` |
99
+ | `tdf datasource-codes` | `GET /data-api/v1/datasource-codes` |
90
100
  | `tdf query` | `POST /data-api/v1/datasets/{id}/query` |
91
101
  | `tdf schema` | `GET /data-api/v1/datasets/{id}/schema` |
92
102
 
93
103
  文档站点默认 `https://data-foundry.tinetcloud.com`,可用 `DF_DOCS_BASE` 覆盖。`tdf docs` 只需要 `DF_AK`。缓存位于 `~/.cache/tinet-data-foundry/docs/<AK 哈希>/`,可用 `DF_DOCS_CACHE` 改根目录。stdout 非终端时 `tdf catalog` 改为 JSON。
94
104
 
95
- ### Skill 查询大目录
105
+ ### 业务 Skill 锁定数据集
96
106
 
97
- `tdf docs pull` 把公共目录与授权策略缓存到本机。Skill / Agent 不应直接把大 `public.json` 读进上下文,而是通过以下命令只取需要的精简结果:
107
+ `tdf docs pull` 把公共目录与授权策略缓存到本机。业务 Skill 自行检索和阅读字段,不要把 `public.json` 或 `policy.json` 读进上下文:
98
108
 
99
109
  ```bash
100
- tdf docs search --query "京东云消费"
110
+ tdf docs pull
111
+ tdf docs search --query "<检索词>"
101
112
  tdf docs describe --dataset "DATASET_ID"
102
113
  ```
103
114
 
104
- `search` 默认只返回有表权限的数据集;`describe` 只返回该表已授权字段及其脱敏规则。首次使用先执行 `tdf docs pull`。
115
+ `search` 默认只返回有表权限的数据集,表名或中文名精确匹配排在前面,最多 20 条。`describe` 只返回该表已授权字段、分区粒度和脱敏规则。需要与网关当前结构核对时再用 `tdf schema --dataset "DATASET_ID"`。不要用不带 `--keyword` 的 `tdf catalog` 拉取整份目录。
105
116
 
106
- Agent / Skill 传查询体用文件,避免 shell 转义:
117
+ DataFoundry 查询 Skill 只执行已经写好的查询。查询体用文件传入,避免 shell 转义:
107
118
 
108
119
  ```bash
109
120
  tdf query --dataset "DATASET_ID" --body-file query.json
@@ -21,12 +21,18 @@ class CliParserTest(unittest.TestCase):
21
21
  self.assertIn("30", help_text)
22
22
  self.assertIn("当天", help_text)
23
23
  self.assertIn("按月、按年分区字段不是必填条件", help_text)
24
+ self.assertIn("_df_pt", help_text)
25
+ self.assertIn("裁分区", help_text)
26
+ self.assertIn("不能作为查询列", help_text)
24
27
  self.assertNotIn("按月补当月", help_text)
25
28
 
26
29
  def test_subcommands(self):
27
30
  parser = build_parser()
28
31
  catalog = parser.parse_args(["catalog", "--format", "json"])
29
32
  self.assertEqual(catalog.cmd, "catalog")
33
+ codes = parser.parse_args(["datasource-codes", "--code", "nboss"])
34
+ self.assertEqual(codes.cmd, "datasource-codes")
35
+ self.assertEqual(codes.code, "nboss")
30
36
  query = parser.parse_args(["query", "--dataset", "ds_1", "--page-no", "1", "--page-size", "10"])
31
37
  self.assertEqual(query.cmd, "query")
32
38
  catalog_k = parser.parse_args(["catalog", "--insecure"])
@@ -17,20 +17,45 @@ class DocsSummaryTest(unittest.TestCase):
17
17
  "description": "京东云消费", "columns": [
18
18
  {"name": "amount", "type": "decimal", "comment": "金额"},
19
19
  {"name": "secret", "type": "text", "comment": "无权限"},
20
+ {"name": "_df_pt", "type": "date", "comment": "分区查询时间",
21
+ "suggestedRange": {"from": "2026-10-01", "to": "2026-10-01", "grain": "month"}},
20
22
  ],
21
23
  }]}), encoding="utf-8")
22
24
  (folder / "policy.json").write_text(json.dumps({"datasets": [{
23
25
  "id": "ds_bill", "tableGranted": True,
24
- "columns": [{"name": "amount", "granted": True, "outputRule": "PLAIN"}],
26
+ "columns": [
27
+ {"name": "amount", "granted": True, "outputRule": "PLAIN"},
28
+ {"name": "_df_pt", "granted": True, "outputRule": "PLAIN"},
29
+ ],
25
30
  }]}), encoding="utf-8")
26
31
  self.assertEqual(_search(folder, "京东云", False), 0)
27
32
  search = json.loads(out.getvalue())
28
33
  self.assertEqual(search["datasets"][0]["id"], "ds_bill")
34
+ self.assertEqual(search["total"], 1)
35
+ self.assertFalse(search["truncated"])
29
36
  out.seek(0)
30
37
  out.truncate(0)
31
38
  self.assertEqual(_describe(folder, "ds_bill"), 0)
32
39
  described = json.loads(out.getvalue())
33
- self.assertEqual([col["name"] for col in described["columns"]], ["amount"])
40
+ self.assertEqual([col["name"] for col in described["columns"]], ["amount", "_df_pt"])
41
+ self.assertEqual(described["columns"][1]["suggestedRange"]["from"], "2026-10-01")
42
+
43
+ def test_search_prefers_exact_name_and_limits_rows(self):
44
+ with tempfile.TemporaryDirectory() as tmp, patch("sys.stdout", new_callable=StringIO) as out:
45
+ folder = Path(tmp)
46
+ datasets = [
47
+ {"id": "ds_desc", "name": "other", "description": "bill detail"},
48
+ {"id": "ds_exact", "name": "bill", "chineseName": "账单"},
49
+ {"id": "ds_more", "name": "bill_extra", "description": "bill"},
50
+ ]
51
+ grants = [{"id": row["id"], "tableGranted": True, "columns": []} for row in datasets]
52
+ (folder / "public.json").write_text(json.dumps({"datasets": datasets}), encoding="utf-8")
53
+ (folder / "policy.json").write_text(json.dumps({"datasets": grants}), encoding="utf-8")
54
+ self.assertEqual(_search(folder, "bill", False, 2), 0)
55
+ search = json.loads(out.getvalue())
56
+ self.assertEqual([row["id"] for row in search["datasets"]], ["ds_exact", "ds_more"])
57
+ self.assertEqual(search["total"], 3)
58
+ self.assertTrue(search["truncated"])
34
59
 
35
60
  def test_summarize_catalog_and_policy(self):
36
61
  old_public = {"datasets": [{"id": "a", "schemaVersion": "1"}, {"id": "b", "schemaVersion": "1"}]}
@@ -1 +0,0 @@
1
- __version__ = "0.1.6"