tinet-data-foundry-cli 0.1.2__tar.gz → 0.1.5__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 (23) hide show
  1. tinet_data_foundry_cli-0.1.5/PKG-INFO +115 -0
  2. tinet_data_foundry_cli-0.1.5/README.md +98 -0
  3. {tinet_data_foundry_cli-0.1.2 → tinet_data_foundry_cli-0.1.5}/pyproject.toml +2 -2
  4. tinet_data_foundry_cli-0.1.5/src/tinet_data_foundry_cli/__init__.py +1 -0
  5. {tinet_data_foundry_cli-0.1.2 → tinet_data_foundry_cli-0.1.5}/src/tinet_data_foundry_cli/cli.py +87 -18
  6. tinet_data_foundry_cli-0.1.5/src/tinet_data_foundry_cli/docs.py +202 -0
  7. tinet_data_foundry_cli-0.1.5/src/tinet_data_foundry_cli.egg-info/PKG-INFO +115 -0
  8. {tinet_data_foundry_cli-0.1.2 → tinet_data_foundry_cli-0.1.5}/src/tinet_data_foundry_cli.egg-info/SOURCES.txt +2 -0
  9. tinet_data_foundry_cli-0.1.5/tests/test_cli.py +95 -0
  10. tinet_data_foundry_cli-0.1.5/tests/test_docs.py +52 -0
  11. tinet_data_foundry_cli-0.1.2/PKG-INFO +0 -71
  12. tinet_data_foundry_cli-0.1.2/README.md +0 -54
  13. tinet_data_foundry_cli-0.1.2/src/tinet_data_foundry_cli/__init__.py +0 -1
  14. tinet_data_foundry_cli-0.1.2/src/tinet_data_foundry_cli.egg-info/PKG-INFO +0 -71
  15. tinet_data_foundry_cli-0.1.2/tests/test_cli.py +0 -26
  16. {tinet_data_foundry_cli-0.1.2 → tinet_data_foundry_cli-0.1.5}/setup.cfg +0 -0
  17. {tinet_data_foundry_cli-0.1.2 → tinet_data_foundry_cli-0.1.5}/src/tinet_data_foundry_cli/__main__.py +0 -0
  18. {tinet_data_foundry_cli-0.1.2 → tinet_data_foundry_cli-0.1.5}/src/tinet_data_foundry_cli/auth.py +0 -0
  19. {tinet_data_foundry_cli-0.1.2 → tinet_data_foundry_cli-0.1.5}/src/tinet_data_foundry_cli/client.py +0 -0
  20. {tinet_data_foundry_cli-0.1.2 → tinet_data_foundry_cli-0.1.5}/src/tinet_data_foundry_cli.egg-info/dependency_links.txt +0 -0
  21. {tinet_data_foundry_cli-0.1.2 → tinet_data_foundry_cli-0.1.5}/src/tinet_data_foundry_cli.egg-info/entry_points.txt +0 -0
  22. {tinet_data_foundry_cli-0.1.2 → tinet_data_foundry_cli-0.1.5}/src/tinet_data_foundry_cli.egg-info/top_level.txt +0 -0
  23. {tinet_data_foundry_cli-0.1.2 → tinet_data_foundry_cli-0.1.5}/tests/test_sign.py +0 -0
@@ -0,0 +1,115 @@
1
+ Metadata-Version: 2.4
2
+ Name: tinet-data-foundry-cli
3
+ Version: 0.1.5
4
+ Summary: Tinet Data Foundry 取数 CLI(docs / catalog / query / schema)
5
+ Author: Tinet
6
+ License: MIT
7
+ Project-URL: Homepage, https://data-foundry.tinetcloud.com
8
+ Project-URL: Documentation, https://data-foundry.tinetcloud.com
9
+ Keywords: data-foundry,tinet,cli
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Environment :: Console
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+
18
+ # tinet-data-foundry-cli
19
+
20
+ Tinet Data Foundry 取数 CLI。包名 `tinet-data-foundry-cli`,命令 **`tdf`**(避开系统磁盘命令 `df`)。
21
+
22
+ 鉴权只用环境变量,**不要**把 AccessKey / SecretKey 写入本地文件。需要 Python **≥ 3.10**。
23
+
24
+ macOS / Linux 用 Terminal(bash / zsh)。Windows 用 **cmd.exe** 跑下面示例(不要用 PowerShell 传 JSON:`export` 不可用,引号也会被剥掉)。
25
+
26
+ ## macOS / Linux
27
+
28
+ ```bash
29
+ pip install -U tinet-data-foundry-cli
30
+
31
+ export DF_ENDPOINT="https://data-foundry-data-api.tinetcloud.com"
32
+ export DF_AK="你的_AccessKey"
33
+ export DF_SK="你的_SecretKey"
34
+
35
+ tdf catalog
36
+ tdf schema --dataset "DATASET_ID"
37
+ tdf query \
38
+ --dataset "DATASET_ID" \
39
+ --fields "id" \
40
+ --filter '{"op":"eq","field":"id","value":"sample"}' \
41
+ --sort '[{"field":"id","direction":"DESC"}]' \
42
+ --page-no 1 \
43
+ --page-size 10 \
44
+ --enterprise-range NORMAL \
45
+ --visitor '{"dingtalkUserId":"钉钉用户ID"}'
46
+ ```
47
+
48
+ `--enterprise-range` 可省略,服务端默认 `NORMAL`(不含注销用户和集团用户)。`ALL` 两者都包含。应用凭证必须传 `--visitor`,与数据集是否配置行级列无关;JSON 里 `username`、`dingtalkUserId`、`dingtalkUserMobile`、`dingtalkUserEmail` 至少填一个。无行级列时仍须传入,但不加企业过滤。个人凭证不要传。
49
+
50
+ 字符串字段可做模糊匹配,`value` 为非空字符串,`%` 和 `_` 按字面匹配:
51
+
52
+ | op | 含义 | 示例 |
53
+ | --- | --- | --- |
54
+ | `starts_with` | 右模糊,匹配前缀 | `{"op":"starts_with","field":"name","value":"北京"}` |
55
+ | `ends_with` | 左模糊,匹配后缀 | `{"op":"ends_with","field":"name","value":"公司"}` |
56
+ | `contains` | 左右模糊 | `{"op":"contains","field":"name","value":"科技"}` |
57
+
58
+ 分区表可以不传分区字段。服务端按上海时区补默认闭区间:按日为当天,按月为当月,按年为当年,并在响应 `data.partitionWindow` 里返回实际 `field/from/to`。显式 `between` 时,按日不超过 30 天,按月不超过 12 个月,按年不超过 3 年,否则 422 `PARTITION_RANGE_INVALID`。
59
+
60
+ 若 pip 提示 scripts 目录不在 PATH,把该目录加入 PATH,或用 `python3 -m tinet_data_foundry_cli`。
61
+
62
+ ## Windows(cmd.exe)
63
+
64
+ ```bat
65
+ pip install -U tinet-data-foundry-cli
66
+
67
+ set DF_ENDPOINT=https://data-foundry-data-api.tinetcloud.com
68
+ set DF_AK=你的_AccessKey
69
+ set DF_SK=你的_SecretKey
70
+
71
+ tdf catalog
72
+ tdf schema --dataset "DATASET_ID"
73
+ tdf query --dataset "DATASET_ID" --fields "id" --filter "{\"op\":\"eq\",\"field\":\"id\",\"value\":\"sample\"}" --sort "[{\"field\":\"id\",\"direction\":\"DESC\"}]" --page-no 1 --page-size 10 --enterprise-range NORMAL --visitor "{\"dingtalkUserId\":\"钉钉用户ID\"}"
74
+ ```
75
+
76
+ 环境变量只对当前 cmd 窗口有效。JSON 参数用 `\"` 转义,整条命令写在一行。
77
+
78
+ 公司代理 / 自签证书导致 `CERTIFICATE_VERIFY_FAILED` 时:
79
+
80
+ ```bash
81
+ tdf --insecure catalog
82
+ tdf catalog --insecure
83
+ ```
84
+
85
+ | 命令 | 接口 |
86
+ | --- | --- |
87
+ | `tdf docs rev` | `GET /docs/credentials/{ak}?format=rev` |
88
+ | `tdf docs pull` | 按 `copy.catalog.policy` 只拉取变化的 `part=public` 或 `part=policy` |
89
+ | `tdf catalog` | `GET /data-api/v1/catalog` |
90
+ | `tdf query` | `POST /data-api/v1/datasets/{id}/query` |
91
+ | `tdf schema` | `GET /data-api/v1/datasets/{id}/schema` |
92
+
93
+ 文档站点默认 `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
+
95
+ Agent / Skill 传查询体用文件,避免 shell 转义:
96
+
97
+ ```bash
98
+ tdf query --dataset "DATASET_ID" --body-file query.json
99
+ ```
100
+
101
+ 签名:`canonical = METHOD + "\n" + PATH + "\n" + TIMESTAMP + "\n" + NONCE + "\n" + hex(SHA256(body))`,`X-DF-Signature = Base64(HMAC-SHA256(canonical, SK))`。GET 的 body 为空字节。PATH 不含域名、不含 query。
102
+
103
+ ## 发布到 PyPI
104
+
105
+ 版本号改 `pyproject.toml` 与 `src/tinet_data_foundry_cli/__init__.py`。
106
+
107
+ ```bash
108
+ python -m pip install -U build twine
109
+ python -m build
110
+ python -m twine check dist/*
111
+ python -m twine upload --repository testpypi dist/*
112
+ python -m twine upload dist/*
113
+ ```
114
+
115
+ 已发布版本号不可覆盖。
@@ -0,0 +1,98 @@
1
+ # tinet-data-foundry-cli
2
+
3
+ Tinet Data Foundry 取数 CLI。包名 `tinet-data-foundry-cli`,命令 **`tdf`**(避开系统磁盘命令 `df`)。
4
+
5
+ 鉴权只用环境变量,**不要**把 AccessKey / SecretKey 写入本地文件。需要 Python **≥ 3.10**。
6
+
7
+ macOS / Linux 用 Terminal(bash / zsh)。Windows 用 **cmd.exe** 跑下面示例(不要用 PowerShell 传 JSON:`export` 不可用,引号也会被剥掉)。
8
+
9
+ ## macOS / Linux
10
+
11
+ ```bash
12
+ pip install -U tinet-data-foundry-cli
13
+
14
+ export DF_ENDPOINT="https://data-foundry-data-api.tinetcloud.com"
15
+ export DF_AK="你的_AccessKey"
16
+ export DF_SK="你的_SecretKey"
17
+
18
+ tdf catalog
19
+ tdf schema --dataset "DATASET_ID"
20
+ tdf query \
21
+ --dataset "DATASET_ID" \
22
+ --fields "id" \
23
+ --filter '{"op":"eq","field":"id","value":"sample"}' \
24
+ --sort '[{"field":"id","direction":"DESC"}]' \
25
+ --page-no 1 \
26
+ --page-size 10 \
27
+ --enterprise-range NORMAL \
28
+ --visitor '{"dingtalkUserId":"钉钉用户ID"}'
29
+ ```
30
+
31
+ `--enterprise-range` 可省略,服务端默认 `NORMAL`(不含注销用户和集团用户)。`ALL` 两者都包含。应用凭证必须传 `--visitor`,与数据集是否配置行级列无关;JSON 里 `username`、`dingtalkUserId`、`dingtalkUserMobile`、`dingtalkUserEmail` 至少填一个。无行级列时仍须传入,但不加企业过滤。个人凭证不要传。
32
+
33
+ 字符串字段可做模糊匹配,`value` 为非空字符串,`%` 和 `_` 按字面匹配:
34
+
35
+ | op | 含义 | 示例 |
36
+ | --- | --- | --- |
37
+ | `starts_with` | 右模糊,匹配前缀 | `{"op":"starts_with","field":"name","value":"北京"}` |
38
+ | `ends_with` | 左模糊,匹配后缀 | `{"op":"ends_with","field":"name","value":"公司"}` |
39
+ | `contains` | 左右模糊 | `{"op":"contains","field":"name","value":"科技"}` |
40
+
41
+ 分区表可以不传分区字段。服务端按上海时区补默认闭区间:按日为当天,按月为当月,按年为当年,并在响应 `data.partitionWindow` 里返回实际 `field/from/to`。显式 `between` 时,按日不超过 30 天,按月不超过 12 个月,按年不超过 3 年,否则 422 `PARTITION_RANGE_INVALID`。
42
+
43
+ 若 pip 提示 scripts 目录不在 PATH,把该目录加入 PATH,或用 `python3 -m tinet_data_foundry_cli`。
44
+
45
+ ## Windows(cmd.exe)
46
+
47
+ ```bat
48
+ pip install -U tinet-data-foundry-cli
49
+
50
+ set DF_ENDPOINT=https://data-foundry-data-api.tinetcloud.com
51
+ set DF_AK=你的_AccessKey
52
+ set DF_SK=你的_SecretKey
53
+
54
+ tdf catalog
55
+ tdf schema --dataset "DATASET_ID"
56
+ tdf query --dataset "DATASET_ID" --fields "id" --filter "{\"op\":\"eq\",\"field\":\"id\",\"value\":\"sample\"}" --sort "[{\"field\":\"id\",\"direction\":\"DESC\"}]" --page-no 1 --page-size 10 --enterprise-range NORMAL --visitor "{\"dingtalkUserId\":\"钉钉用户ID\"}"
57
+ ```
58
+
59
+ 环境变量只对当前 cmd 窗口有效。JSON 参数用 `\"` 转义,整条命令写在一行。
60
+
61
+ 公司代理 / 自签证书导致 `CERTIFICATE_VERIFY_FAILED` 时:
62
+
63
+ ```bash
64
+ tdf --insecure catalog
65
+ tdf catalog --insecure
66
+ ```
67
+
68
+ | 命令 | 接口 |
69
+ | --- | --- |
70
+ | `tdf docs rev` | `GET /docs/credentials/{ak}?format=rev` |
71
+ | `tdf docs pull` | 按 `copy.catalog.policy` 只拉取变化的 `part=public` 或 `part=policy` |
72
+ | `tdf catalog` | `GET /data-api/v1/catalog` |
73
+ | `tdf query` | `POST /data-api/v1/datasets/{id}/query` |
74
+ | `tdf schema` | `GET /data-api/v1/datasets/{id}/schema` |
75
+
76
+ 文档站点默认 `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
+
78
+ Agent / Skill 传查询体用文件,避免 shell 转义:
79
+
80
+ ```bash
81
+ tdf query --dataset "DATASET_ID" --body-file query.json
82
+ ```
83
+
84
+ 签名:`canonical = METHOD + "\n" + PATH + "\n" + TIMESTAMP + "\n" + NONCE + "\n" + hex(SHA256(body))`,`X-DF-Signature = Base64(HMAC-SHA256(canonical, SK))`。GET 的 body 为空字节。PATH 不含域名、不含 query。
85
+
86
+ ## 发布到 PyPI
87
+
88
+ 版本号改 `pyproject.toml` 与 `src/tinet_data_foundry_cli/__init__.py`。
89
+
90
+ ```bash
91
+ python -m pip install -U build twine
92
+ python -m build
93
+ python -m twine check dist/*
94
+ python -m twine upload --repository testpypi dist/*
95
+ python -m twine upload dist/*
96
+ ```
97
+
98
+ 已发布版本号不可覆盖。
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tinet-data-foundry-cli"
7
- version = "0.1.2"
8
- description = "Tinet Data Foundry 取数 CLI(catalog / query / schema)"
7
+ version = "0.1.5"
8
+ description = "Tinet Data Foundry 取数 CLI(docs / catalog / query / schema)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
11
11
  license = { text = "MIT" }
@@ -0,0 +1 @@
1
+ __version__ = "0.1.5"
@@ -105,7 +105,10 @@ def cmd_catalog(args: argparse.Namespace) -> int:
105
105
  _exit_api(ex)
106
106
  except TransportError as ex:
107
107
  _exit_transport(ex)
108
- if args.dry_run or args.format == "json":
108
+ fmt = args.format
109
+ if fmt == "table" and not sys.stdout.isatty():
110
+ fmt = "json"
111
+ if args.dry_run or fmt == "json":
109
112
  _print_json(parsed)
110
113
  return 0
111
114
  data = _ok_data(parsed) or {}
@@ -114,23 +117,60 @@ def cmd_catalog(args: argparse.Namespace) -> int:
114
117
  return 0
115
118
 
116
119
 
120
+ def _query_body(args: argparse.Namespace) -> Optional[dict]:
121
+ if args.body and args.body_file:
122
+ raise SystemExit("--body 与 --body-file 只能选一个")
123
+ raw: Optional[str] = None
124
+ if args.body == "-":
125
+ raw = sys.stdin.read()
126
+ elif args.body:
127
+ raw = args.body
128
+ elif args.body_file:
129
+ try:
130
+ with open(args.body_file, encoding="utf-8") as fh:
131
+ raw = fh.read()
132
+ except OSError as ex:
133
+ raise SystemExit(f"无法读取 --body-file:{ex}") from ex
134
+ if raw is None:
135
+ return None
136
+ for flag, value in (
137
+ ("--filter", args.filter),
138
+ ("--fields", args.fields),
139
+ ("--sort", args.sort),
140
+ ("--visitor", args.visitor),
141
+ ("--enterprise-range", args.enterprise_range),
142
+ ):
143
+ if value:
144
+ raise SystemExit(f"使用 --body / --body-file 时不要再传 {flag}")
145
+ parsed = _parse_json(raw, "--body")
146
+ if not isinstance(parsed, dict):
147
+ raise SystemExit("查询 body 必须是 JSON 对象")
148
+ return parsed
149
+
150
+
117
151
  def cmd_query(args: argparse.Namespace) -> int:
118
152
  base, ak, sk = credentials(args.endpoint)
119
- payload: dict[str, Any] = {
120
- "pageNo": args.page_no,
121
- "pageSize": args.page_size,
122
- }
123
- if args.fields:
124
- payload["fields"] = [p.strip() for p in args.fields.split(",") if p.strip()]
125
- filt = _parse_json(args.filter, "--filter")
126
- if filt is not None:
127
- payload["filter"] = filt
128
- sort = _parse_json(args.sort, "--sort")
129
- if sort is not None:
130
- payload["sort"] = sort
131
- visitor = _parse_json(args.visitor, "--visitor")
132
- if visitor is not None:
133
- payload["visitor"] = visitor
153
+ body_doc = _query_body(args)
154
+ if body_doc is not None:
155
+ payload: dict[str, Any] = body_doc
156
+ else:
157
+ payload = {
158
+ "pageNo": args.page_no,
159
+ "pageSize": args.page_size,
160
+ }
161
+ if args.fields:
162
+ payload["fields"] = [p.strip() for p in args.fields.split(",") if p.strip()]
163
+ filt = _parse_json(args.filter, "--filter")
164
+ if filt is not None:
165
+ payload["filter"] = filt
166
+ sort = _parse_json(args.sort, "--sort")
167
+ if sort is not None:
168
+ payload["sort"] = sort
169
+ visitor = _parse_json(args.visitor, "--visitor")
170
+ if visitor is not None:
171
+ payload["visitor"] = visitor
172
+ if args.enterprise_range:
173
+ payload["enterpriseRange"] = args.enterprise_range
134
174
  body = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
135
175
  path = f"/data-api/v1/datasets/{args.dataset}/query"
136
176
  try:
@@ -212,14 +252,37 @@ def build_parser() -> argparse.ArgumentParser:
212
252
  q = sub.add_parser("query", help="查询 Dataset")
213
253
  _add_insecure(q, overlay=True)
214
254
  q.add_argument("--dataset", required=True)
255
+ q.add_argument("--body", help="完整查询 JSON。- 表示从 stdin 读取。与 --filter 等参数互斥")
256
+ q.add_argument("--body-file", help="完整查询 JSON 文件。Agent / Skill 用此参数,避免 shell 转义")
215
257
  q.add_argument("--fields", help="逗号分隔列名")
216
- q.add_argument("--filter", help="filter AST JSON")
258
+ q.add_argument(
259
+ "--filter",
260
+ help=(
261
+ "filter AST JSON。字符串字段可 starts_with(右模糊)、ends_with(左模糊)、contains(左右模糊)。"
262
+ "分区表省略分区字段时,按日补当天、按月补当月、按年补当年;"
263
+ "显式 between 按日不超过 30 天、按月不超过 12 个月、按年不超过 3 年"
264
+ ),
265
+ )
217
266
  q.add_argument("--sort", help="sort JSON 数组")
218
267
  q.add_argument("--page-no", type=int, default=1)
219
268
  q.add_argument("--page-size", type=int, default=10)
220
- q.add_argument("--visitor", help="应用凭证 visitor JSON")
269
+ q.add_argument(
270
+ "--enterprise-range",
271
+ choices=["NORMAL", "ALL"],
272
+ help="检索企业范围。省略时服务端按 NORMAL(不含注销用户和集团用户);ALL 两者都包含",
273
+ )
274
+ q.add_argument(
275
+ "--visitor",
276
+ help="应用凭证查询身份 JSON。username、dingtalkUserId、dingtalkUserMobile、dingtalkUserEmail 至少填一个;个人凭证不要传",
277
+ )
221
278
  q.set_defaults(func=cmd_query)
222
279
 
280
+ d = sub.add_parser("docs", help="核对数据集开发文档修订号并缓存 public/policy")
281
+ _add_insecure(d, overlay=True)
282
+ d.add_argument("action", choices=["rev", "pull"])
283
+ d.add_argument("--docs-base", help="覆盖文档站点。默认 DF_DOCS_BASE 或 https://data-foundry.tinetcloud.com")
284
+ d.set_defaults(func=cmd_docs)
285
+
223
286
  s = sub.add_parser("schema", help="拉取 Dataset Schema")
224
287
  _add_insecure(s, overlay=True)
225
288
  s.add_argument("--dataset", required=True)
@@ -227,6 +290,12 @@ def build_parser() -> argparse.ArgumentParser:
227
290
  return p
228
291
 
229
292
 
293
+ def cmd_docs(args: argparse.Namespace) -> int:
294
+ from tinet_data_foundry_cli.docs import run_docs
295
+
296
+ return run_docs(args)
297
+
298
+
230
299
  def main(argv: Optional[List[str]] = None) -> int:
231
300
  parser = build_parser()
232
301
  args = parser.parse_args(argv)
@@ -0,0 +1,202 @@
1
+ """数据集开发文档:修订号核对与分段缓存。文档接口不签名、不使用 SK。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import os
8
+ import ssl
9
+ import sys
10
+ import urllib.error
11
+ import urllib.parse
12
+ import urllib.request
13
+ from pathlib import Path
14
+ from typing import Any, Dict, List, Optional
15
+
16
+ DEFAULT_DOCS_BASE = "https://data-foundry.tinetcloud.com"
17
+
18
+
19
+ def access_key() -> str:
20
+ ak = (os.environ.get("DF_AK") or "").strip()
21
+ if not ak:
22
+ raise SystemExit("缺少 DF_AK 环境变量(不要写入本地配置文件)")
23
+ return ak
24
+
25
+
26
+ def docs_base(override: Optional[str] = None) -> str:
27
+ raw = (override or os.environ.get("DF_DOCS_BASE") or DEFAULT_DOCS_BASE).strip()
28
+ return raw.rstrip("/")
29
+
30
+
31
+ def cache_dir(ak: str) -> Path:
32
+ digest = hashlib.sha256(ak.encode("utf-8")).hexdigest()[:16]
33
+ root = (os.environ.get("DF_DOCS_CACHE") or "").strip()
34
+ base = Path(root) if root else Path.home() / ".cache" / "tinet-data-foundry" / "docs"
35
+ return base / digest
36
+
37
+
38
+ def docs_url(base: str, ak: str, fmt: str, part: Optional[str] = None) -> str:
39
+ path = "/docs/credentials/" + urllib.parse.quote(ak, safe="")
40
+ query = {"format": fmt}
41
+ if part:
42
+ query["part"] = part
43
+ return base + path + "?" + urllib.parse.urlencode(query)
44
+
45
+
46
+ def run_docs(args: Any) -> int:
47
+ ak = access_key()
48
+ base = docs_base(getattr(args, "docs_base", None))
49
+ folder = cache_dir(ak)
50
+ url = docs_url(base, ak, "rev")
51
+ if args.dry_run:
52
+ _print({
53
+ "dryRun": True,
54
+ "action": args.action,
55
+ "url": url,
56
+ "cacheDir": str(folder),
57
+ })
58
+ return 0
59
+ if args.action == "rev":
60
+ status, payload = _get_json(url, None, args.timeout, args.insecure)
61
+ _ensure_ok(payload)
62
+ _print(payload if status != 304 else _read_json(folder / "rev.json"))
63
+ return 0
64
+ return _pull(folder, base, ak, args.timeout, args.insecure)
65
+
66
+
67
+ def _pull(folder: Path, base: str, ak: str, timeout: float, insecure: bool) -> int:
68
+ local = _read_json(folder / "rev.json")
69
+ local_etag = str(local.get("etag") or "")
70
+ status, remote = _get_json(docs_url(base, ak, "rev"), local_etag or None, timeout, insecure)
71
+ if status == 304:
72
+ _print({
73
+ "status": "unchanged",
74
+ "etag": local_etag,
75
+ "changed": [],
76
+ "cacheDir": str(folder),
77
+ "summary": [],
78
+ })
79
+ return 0
80
+ _ensure_ok(remote)
81
+ etag = str(remote.get("etag") or "")
82
+ if etag and etag == local_etag and (folder / "public.json").is_file() and (folder / "policy.json").is_file():
83
+ _print({
84
+ "status": "unchanged",
85
+ "etag": etag,
86
+ "changed": [],
87
+ "cacheDir": str(folder),
88
+ "summary": [],
89
+ })
90
+ return 0
91
+ changed: List[str] = []
92
+ old_public = _read_json(folder / "public.json")
93
+ old_policy = _read_json(folder / "policy.json")
94
+ if not local_etag or local.get("copy") != remote.get("copy") or local.get("catalog") != remote.get("catalog"):
95
+ _, public = _get_json(docs_url(base, ak, "json", "public"), None, timeout, insecure)
96
+ _ensure_ok(public)
97
+ _write_json(folder / "public.json", public)
98
+ changed.append("public")
99
+ else:
100
+ public = old_public
101
+ if not local_etag or local.get("policy") != remote.get("policy"):
102
+ _, policy = _get_json(docs_url(base, ak, "json", "policy"), None, timeout, insecure)
103
+ _ensure_ok(policy)
104
+ _write_json(folder / "policy.json", policy)
105
+ changed.append("policy")
106
+ else:
107
+ policy = old_policy
108
+ _write_json(folder / "rev.json", {
109
+ "copy": remote.get("copy"),
110
+ "catalog": remote.get("catalog"),
111
+ "policy": remote.get("policy"),
112
+ "etag": etag,
113
+ })
114
+ _print({
115
+ "status": "updated",
116
+ "etag": etag,
117
+ "changed": changed,
118
+ "cacheDir": str(folder),
119
+ "summary": summarize(old_public, public, old_policy, policy) if changed else [],
120
+ })
121
+ return 0
122
+
123
+
124
+ def summarize(old_public: Dict[str, Any], new_public: Dict[str, Any],
125
+ old_policy: Dict[str, Any], new_policy: Dict[str, Any]) -> List[str]:
126
+ if not old_public and not old_policy:
127
+ return ["首次拉取"]
128
+ lines: List[str] = []
129
+ old_ds = _index(old_public)
130
+ new_ds = _index(new_public)
131
+ for dataset_id in sorted(set(new_ds) - set(old_ds)):
132
+ lines.append(f"新增数据集 {dataset_id}")
133
+ for dataset_id in sorted(set(old_ds) - set(new_ds)):
134
+ lines.append(f"下线数据集 {dataset_id}")
135
+ for dataset_id in sorted(set(old_ds) & set(new_ds)):
136
+ if str(old_ds[dataset_id].get("schemaVersion") or "") != str(new_ds[dataset_id].get("schemaVersion") or ""):
137
+ lines.append(f"schemaVersion 变化 {dataset_id}")
138
+ old_grants = _index(old_policy)
139
+ new_grants = _index(new_policy)
140
+ for dataset_id in sorted(set(old_grants) | set(new_grants)):
141
+ before = bool(old_grants.get(dataset_id, {}).get("tableGranted"))
142
+ after = bool(new_grants.get(dataset_id, {}).get("tableGranted"))
143
+ if before != after:
144
+ lines.append(f"表权限变化 {dataset_id} {'有' if after else '无'}")
145
+ return lines[:30]
146
+
147
+
148
+ def _index(doc: Dict[str, Any]) -> Dict[str, Dict[str, Any]]:
149
+ out: Dict[str, Dict[str, Any]] = {}
150
+ for row in doc.get("datasets") or []:
151
+ if isinstance(row, dict) and row.get("id"):
152
+ out[str(row["id"])] = row
153
+ return out
154
+
155
+
156
+ def _get_json(url: str, etag: Optional[str], timeout: float, insecure: bool):
157
+ headers = {"Accept": "application/json"}
158
+ if etag:
159
+ headers["If-None-Match"] = '"' + etag + '"'
160
+ req = urllib.request.Request(url, headers=headers, method="GET")
161
+ ctx = ssl._create_unverified_context() if insecure else None
162
+ try:
163
+ with urllib.request.urlopen(req, timeout=timeout, context=ctx) as resp:
164
+ raw = resp.read()
165
+ payload = json.loads(raw.decode("utf-8")) if raw else {}
166
+ return resp.status, payload
167
+ except urllib.error.HTTPError as ex:
168
+ if ex.code == 304:
169
+ return 304, {}
170
+ raw = ex.read() if ex.fp else b""
171
+ try:
172
+ payload = json.loads(raw.decode("utf-8")) if raw else {"message": str(ex)}
173
+ except json.JSONDecodeError:
174
+ payload = {"message": raw.decode("utf-8", errors="replace")}
175
+ raise SystemExit(f"文档请求失败 HTTP {ex.code}: {payload.get('message', payload)}") from ex
176
+ except (urllib.error.URLError, ssl.SSLError, TimeoutError, OSError) as ex:
177
+ raise SystemExit(f"无法连接文档站点:{ex}") from ex
178
+
179
+
180
+ def _ensure_ok(payload: Any) -> None:
181
+ if isinstance(payload, dict) and payload.get("code") not in (None, 0, "0"):
182
+ raise SystemExit(str(payload.get("message") or payload))
183
+
184
+
185
+ def _read_json(path: Path) -> Dict[str, Any]:
186
+ if not path.is_file():
187
+ return {}
188
+ try:
189
+ data = json.loads(path.read_text(encoding="utf-8"))
190
+ except (OSError, json.JSONDecodeError):
191
+ return {}
192
+ return data if isinstance(data, dict) else {}
193
+
194
+
195
+ def _write_json(path: Path, payload: Any) -> None:
196
+ path.parent.mkdir(parents=True, exist_ok=True)
197
+ path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
198
+
199
+
200
+ def _print(payload: Any) -> None:
201
+ json.dump(payload, sys.stdout, ensure_ascii=False, indent=2)
202
+ sys.stdout.write("\n")
@@ -0,0 +1,115 @@
1
+ Metadata-Version: 2.4
2
+ Name: tinet-data-foundry-cli
3
+ Version: 0.1.5
4
+ Summary: Tinet Data Foundry 取数 CLI(docs / catalog / query / schema)
5
+ Author: Tinet
6
+ License: MIT
7
+ Project-URL: Homepage, https://data-foundry.tinetcloud.com
8
+ Project-URL: Documentation, https://data-foundry.tinetcloud.com
9
+ Keywords: data-foundry,tinet,cli
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Environment :: Console
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+
18
+ # tinet-data-foundry-cli
19
+
20
+ Tinet Data Foundry 取数 CLI。包名 `tinet-data-foundry-cli`,命令 **`tdf`**(避开系统磁盘命令 `df`)。
21
+
22
+ 鉴权只用环境变量,**不要**把 AccessKey / SecretKey 写入本地文件。需要 Python **≥ 3.10**。
23
+
24
+ macOS / Linux 用 Terminal(bash / zsh)。Windows 用 **cmd.exe** 跑下面示例(不要用 PowerShell 传 JSON:`export` 不可用,引号也会被剥掉)。
25
+
26
+ ## macOS / Linux
27
+
28
+ ```bash
29
+ pip install -U tinet-data-foundry-cli
30
+
31
+ export DF_ENDPOINT="https://data-foundry-data-api.tinetcloud.com"
32
+ export DF_AK="你的_AccessKey"
33
+ export DF_SK="你的_SecretKey"
34
+
35
+ tdf catalog
36
+ tdf schema --dataset "DATASET_ID"
37
+ tdf query \
38
+ --dataset "DATASET_ID" \
39
+ --fields "id" \
40
+ --filter '{"op":"eq","field":"id","value":"sample"}' \
41
+ --sort '[{"field":"id","direction":"DESC"}]' \
42
+ --page-no 1 \
43
+ --page-size 10 \
44
+ --enterprise-range NORMAL \
45
+ --visitor '{"dingtalkUserId":"钉钉用户ID"}'
46
+ ```
47
+
48
+ `--enterprise-range` 可省略,服务端默认 `NORMAL`(不含注销用户和集团用户)。`ALL` 两者都包含。应用凭证必须传 `--visitor`,与数据集是否配置行级列无关;JSON 里 `username`、`dingtalkUserId`、`dingtalkUserMobile`、`dingtalkUserEmail` 至少填一个。无行级列时仍须传入,但不加企业过滤。个人凭证不要传。
49
+
50
+ 字符串字段可做模糊匹配,`value` 为非空字符串,`%` 和 `_` 按字面匹配:
51
+
52
+ | op | 含义 | 示例 |
53
+ | --- | --- | --- |
54
+ | `starts_with` | 右模糊,匹配前缀 | `{"op":"starts_with","field":"name","value":"北京"}` |
55
+ | `ends_with` | 左模糊,匹配后缀 | `{"op":"ends_with","field":"name","value":"公司"}` |
56
+ | `contains` | 左右模糊 | `{"op":"contains","field":"name","value":"科技"}` |
57
+
58
+ 分区表可以不传分区字段。服务端按上海时区补默认闭区间:按日为当天,按月为当月,按年为当年,并在响应 `data.partitionWindow` 里返回实际 `field/from/to`。显式 `between` 时,按日不超过 30 天,按月不超过 12 个月,按年不超过 3 年,否则 422 `PARTITION_RANGE_INVALID`。
59
+
60
+ 若 pip 提示 scripts 目录不在 PATH,把该目录加入 PATH,或用 `python3 -m tinet_data_foundry_cli`。
61
+
62
+ ## Windows(cmd.exe)
63
+
64
+ ```bat
65
+ pip install -U tinet-data-foundry-cli
66
+
67
+ set DF_ENDPOINT=https://data-foundry-data-api.tinetcloud.com
68
+ set DF_AK=你的_AccessKey
69
+ set DF_SK=你的_SecretKey
70
+
71
+ tdf catalog
72
+ tdf schema --dataset "DATASET_ID"
73
+ tdf query --dataset "DATASET_ID" --fields "id" --filter "{\"op\":\"eq\",\"field\":\"id\",\"value\":\"sample\"}" --sort "[{\"field\":\"id\",\"direction\":\"DESC\"}]" --page-no 1 --page-size 10 --enterprise-range NORMAL --visitor "{\"dingtalkUserId\":\"钉钉用户ID\"}"
74
+ ```
75
+
76
+ 环境变量只对当前 cmd 窗口有效。JSON 参数用 `\"` 转义,整条命令写在一行。
77
+
78
+ 公司代理 / 自签证书导致 `CERTIFICATE_VERIFY_FAILED` 时:
79
+
80
+ ```bash
81
+ tdf --insecure catalog
82
+ tdf catalog --insecure
83
+ ```
84
+
85
+ | 命令 | 接口 |
86
+ | --- | --- |
87
+ | `tdf docs rev` | `GET /docs/credentials/{ak}?format=rev` |
88
+ | `tdf docs pull` | 按 `copy.catalog.policy` 只拉取变化的 `part=public` 或 `part=policy` |
89
+ | `tdf catalog` | `GET /data-api/v1/catalog` |
90
+ | `tdf query` | `POST /data-api/v1/datasets/{id}/query` |
91
+ | `tdf schema` | `GET /data-api/v1/datasets/{id}/schema` |
92
+
93
+ 文档站点默认 `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
+
95
+ Agent / Skill 传查询体用文件,避免 shell 转义:
96
+
97
+ ```bash
98
+ tdf query --dataset "DATASET_ID" --body-file query.json
99
+ ```
100
+
101
+ 签名:`canonical = METHOD + "\n" + PATH + "\n" + TIMESTAMP + "\n" + NONCE + "\n" + hex(SHA256(body))`,`X-DF-Signature = Base64(HMAC-SHA256(canonical, SK))`。GET 的 body 为空字节。PATH 不含域名、不含 query。
102
+
103
+ ## 发布到 PyPI
104
+
105
+ 版本号改 `pyproject.toml` 与 `src/tinet_data_foundry_cli/__init__.py`。
106
+
107
+ ```bash
108
+ python -m pip install -U build twine
109
+ python -m build
110
+ python -m twine check dist/*
111
+ python -m twine upload --repository testpypi dist/*
112
+ python -m twine upload dist/*
113
+ ```
114
+
115
+ 已发布版本号不可覆盖。
@@ -5,10 +5,12 @@ src/tinet_data_foundry_cli/__main__.py
5
5
  src/tinet_data_foundry_cli/auth.py
6
6
  src/tinet_data_foundry_cli/cli.py
7
7
  src/tinet_data_foundry_cli/client.py
8
+ src/tinet_data_foundry_cli/docs.py
8
9
  src/tinet_data_foundry_cli.egg-info/PKG-INFO
9
10
  src/tinet_data_foundry_cli.egg-info/SOURCES.txt
10
11
  src/tinet_data_foundry_cli.egg-info/dependency_links.txt
11
12
  src/tinet_data_foundry_cli.egg-info/entry_points.txt
12
13
  src/tinet_data_foundry_cli.egg-info/top_level.txt
13
14
  tests/test_cli.py
15
+ tests/test_docs.py
14
16
  tests/test_sign.py
@@ -0,0 +1,95 @@
1
+ import json
2
+ import unittest
3
+ from io import StringIO
4
+ from unittest.mock import patch
5
+
6
+ from tinet_data_foundry_cli.cli import build_parser, main
7
+
8
+
9
+ class CliParserTest(unittest.TestCase):
10
+ def test_query_filter_help_mentions_string_match(self):
11
+ parser = build_parser()
12
+ with patch("sys.stdout", new_callable=StringIO) as out:
13
+ with self.assertRaises(SystemExit) as ctx:
14
+ parser.parse_args(["query", "--help"])
15
+ self.assertEqual(ctx.exception.code, 0)
16
+ help_text = out.getvalue()
17
+ self.assertIn("starts_with", help_text)
18
+ self.assertIn("ends_with", help_text)
19
+ self.assertIn("contains", help_text)
20
+ self.assertIn("30", help_text)
21
+ self.assertIn("当天", help_text)
22
+
23
+ def test_subcommands(self):
24
+ parser = build_parser()
25
+ catalog = parser.parse_args(["catalog", "--format", "json"])
26
+ self.assertEqual(catalog.cmd, "catalog")
27
+ query = parser.parse_args(["query", "--dataset", "ds_1", "--page-no", "1", "--page-size", "10"])
28
+ self.assertEqual(query.cmd, "query")
29
+ catalog_k = parser.parse_args(["catalog", "--insecure"])
30
+ self.assertTrue(catalog_k.insecure)
31
+ catalog_before = parser.parse_args(["--insecure", "catalog"])
32
+ self.assertTrue(catalog_before.insecure)
33
+
34
+ def test_query_enterprise_range_and_visitor(self):
35
+ parser = build_parser()
36
+ query = parser.parse_args([
37
+ "query", "--dataset", "ds_1", "--enterprise-range", "ALL",
38
+ "--visitor", "{\"dingtalkUserId\":\"dt-1\"}",
39
+ ])
40
+ self.assertEqual(query.enterprise_range, "ALL")
41
+ env = {"DF_ENDPOINT": "https://example.test", "DF_AK": "ak", "DF_SK": "sk" * 8}
42
+ with patch.dict("os.environ", env, clear=False), patch("sys.stdout", new_callable=StringIO) as out:
43
+ code = main([
44
+ "--dry-run", "query", "--dataset", "ds_1", "--enterprise-range", "ALL",
45
+ "--visitor", "{\"dingtalkUserId\":\"dt-1\"}",
46
+ ])
47
+ self.assertEqual(code, 0)
48
+ body = json.loads(json.loads(out.getvalue())["body"])
49
+ self.assertEqual(body["enterpriseRange"], "ALL")
50
+ self.assertEqual(body["visitor"]["dingtalkUserId"], "dt-1")
51
+ self.assertNotIn("enterpriseRange", json.loads(json.loads(
52
+ self._dry_query(["--dry-run", "query", "--dataset", "ds_1"])
53
+ )["body"]))
54
+
55
+ def _dry_query(self, argv):
56
+ env = {"DF_ENDPOINT": "https://example.test", "DF_AK": "ak", "DF_SK": "sk" * 8}
57
+ with patch.dict("os.environ", env, clear=False), patch("sys.stdout", new_callable=StringIO) as out:
58
+ self.assertEqual(main(argv), 0)
59
+ return out.getvalue()
60
+
61
+ def test_dry_run_catalog_does_not_need_network(self):
62
+ with patch.dict("os.environ", {"DF_AK": "ak", "DF_SK": "sk" * 8}, clear=False):
63
+ code = main(["--dry-run", "catalog"])
64
+ self.assertEqual(code, 0)
65
+
66
+ def test_query_body_file_is_the_post_body(self):
67
+ import tempfile
68
+ payload = {"fields": ["id"], "pageNo": 1, "pageSize": 5, "filter": {"op": "eq", "field": "id", "value": "a"}}
69
+ with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False, encoding="utf-8") as fh:
70
+ json.dump(payload, fh)
71
+ path = fh.name
72
+ env = {"DF_ENDPOINT": "https://example.test", "DF_AK": "ak", "DF_SK": "sk" * 8}
73
+ with patch.dict("os.environ", env, clear=False), patch("sys.stdout", new_callable=StringIO) as out:
74
+ code = main(["--dry-run", "query", "--dataset", "ds_1", "--body-file", path])
75
+ self.assertEqual(code, 0)
76
+ body = json.loads(json.loads(out.getvalue())["body"])
77
+ self.assertEqual(body["fields"], ["id"])
78
+ self.assertEqual(body["pageSize"], 5)
79
+ with patch.dict("os.environ", env, clear=False):
80
+ with self.assertRaises(SystemExit):
81
+ main(["--dry-run", "query", "--dataset", "ds_1", "--body-file", path, "--filter", "{}"])
82
+
83
+ def test_docs_pull_dry_run_does_not_need_network(self):
84
+ env = {"DF_AK": "DFAK_example"}
85
+ with patch.dict("os.environ", env, clear=False), patch("sys.stdout", new_callable=StringIO) as out:
86
+ code = main(["--dry-run", "docs", "pull"])
87
+ self.assertEqual(code, 0)
88
+ printed = json.loads(out.getvalue())
89
+ self.assertTrue(printed["dryRun"])
90
+ self.assertIn("/docs/credentials/DFAK_example?format=rev", printed["url"])
91
+ self.assertNotIn("sk", printed)
92
+
93
+
94
+ if __name__ == "__main__":
95
+ unittest.main()
@@ -0,0 +1,52 @@
1
+ import json
2
+ import tempfile
3
+ import unittest
4
+ from pathlib import Path
5
+ from unittest.mock import patch
6
+
7
+ from tinet_data_foundry_cli.docs import summarize
8
+
9
+
10
+ class DocsSummaryTest(unittest.TestCase):
11
+ def test_summarize_catalog_and_policy(self):
12
+ old_public = {"datasets": [{"id": "a", "schemaVersion": "1"}, {"id": "b", "schemaVersion": "1"}]}
13
+ new_public = {"datasets": [{"id": "b", "schemaVersion": "2"}, {"id": "c", "schemaVersion": "1"}]}
14
+ old_policy = {"datasets": [{"id": "a", "tableGranted": True}, {"id": "b", "tableGranted": False}]}
15
+ new_policy = {"datasets": [{"id": "b", "tableGranted": True}, {"id": "c", "tableGranted": False}]}
16
+ lines = summarize(old_public, new_public, old_policy, new_policy)
17
+ self.assertIn("新增数据集 c", lines)
18
+ self.assertIn("下线数据集 a", lines)
19
+ self.assertIn("schemaVersion 变化 b", lines)
20
+ self.assertIn("表权限变化 b 有", lines)
21
+
22
+ def test_pull_fetches_only_policy_when_catalog_unchanged(self):
23
+ from tinet_data_foundry_cli import docs as docs_mod
24
+
25
+ rev = {"copy": "c", "catalog": "k", "policy": "p2", "etag": "c.k.p2"}
26
+ calls = []
27
+
28
+ def fake_get(url, etag, timeout, insecure):
29
+ calls.append(url)
30
+ if "format=rev" in url:
31
+ return 200, rev
32
+ if "part=policy" in url:
33
+ return 200, {"credentialType": "PERSONAL", "datasets": [{"id": "a", "tableGranted": True, "columns": []}]}
34
+ raise AssertionError(url)
35
+
36
+ with tempfile.TemporaryDirectory() as tmp:
37
+ folder = Path(tmp)
38
+ (folder / "rev.json").write_text(json.dumps({
39
+ "copy": "c", "catalog": "k", "policy": "p1", "etag": "c.k.p1",
40
+ }), encoding="utf-8")
41
+ (folder / "public.json").write_text(json.dumps({"datasets": [{"id": "a"}]}), encoding="utf-8")
42
+ with patch.object(docs_mod, "_get_json", fake_get):
43
+ code = docs_mod._pull(folder, "https://docs.example", "DFAK_x", 5, False)
44
+ self.assertEqual(code, 0)
45
+ self.assertTrue(any("part=policy" in url for url in calls))
46
+ self.assertFalse(any("part=public" in url for url in calls))
47
+ saved = json.loads((folder / "rev.json").read_text(encoding="utf-8"))
48
+ self.assertEqual(saved["policy"], "p2")
49
+
50
+
51
+ if __name__ == "__main__":
52
+ unittest.main()
@@ -1,71 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: tinet-data-foundry-cli
3
- Version: 0.1.2
4
- Summary: Tinet Data Foundry 取数 CLI(catalog / query / schema)
5
- Author: Tinet
6
- License: MIT
7
- Project-URL: Homepage, https://data-foundry.tinetcloud.com
8
- Project-URL: Documentation, https://data-foundry.tinetcloud.com
9
- Keywords: data-foundry,tinet,cli
10
- Classifier: Programming Language :: Python :: 3
11
- Classifier: Programming Language :: Python :: 3.10
12
- Classifier: Programming Language :: Python :: 3.11
13
- Classifier: Programming Language :: Python :: 3.12
14
- Classifier: Environment :: Console
15
- Requires-Python: >=3.10
16
- Description-Content-Type: text/markdown
17
-
18
- # tinet-data-foundry-cli
19
-
20
- Tinet Data Foundry 取数 CLI。包名 `tinet-data-foundry-cli`,命令 **`tdf`**(避开系统磁盘命令 `df`)。
21
-
22
- 鉴权只用环境变量,**不要**把 AccessKey / SecretKey 写入本地文件。
23
-
24
- ```bash
25
- pip install -U tinet-data-foundry-cli
26
-
27
- export DF_ENDPOINT="https://data-foundry-data-api.tinetcloud.com"
28
- export DF_AK="你的_AccessKey"
29
- export DF_SK="你的_SecretKey"
30
-
31
- tdf catalog
32
- tdf schema --dataset "DATASET_ID"
33
- tdf query \
34
- --dataset "DATASET_ID" \
35
- --fields "id" \
36
- --filter '{"op":"eq","field":"id","value":"sample"}' \
37
- --sort '[{"field":"id","direction":"DESC"}]' \
38
- --page-no 1 \
39
- --page-size 10
40
- ```
41
-
42
- 需要 Python **≥ 3.10**。若 pip 提示 scripts 目录不在 PATH,把该目录加入 PATH,或用 `python3 -m tinet_data_foundry_cli`。
43
-
44
- 公司代理 / 自签证书导致 `CERTIFICATE_VERIFY_FAILED` 时:
45
-
46
- ```bash
47
- tdf --insecure catalog
48
- tdf catalog --insecure
49
- ```
50
-
51
- | 命令 | 接口 |
52
- | --- | --- |
53
- | `tdf catalog` | `GET /data-api/v1/catalog` |
54
- | `tdf query` | `POST /data-api/v1/datasets/{id}/query` |
55
- | `tdf schema` | `GET /data-api/v1/datasets/{id}/schema` |
56
-
57
- 签名:`canonical = METHOD + "\n" + PATH + "\n" + TIMESTAMP + "\n" + NONCE + "\n" + hex(SHA256(body))`,`X-DF-Signature = Base64(HMAC-SHA256(canonical, SK))`。GET 的 body 为空字节。PATH 不含域名、不含 query。
58
-
59
- ## 发布到 PyPI
60
-
61
- 版本号改 `pyproject.toml` 与 `src/tinet_data_foundry_cli/__init__.py`。
62
-
63
- ```bash
64
- python -m pip install -U build twine
65
- python -m build
66
- python -m twine check dist/*
67
- python -m twine upload --repository testpypi dist/*
68
- python -m twine upload dist/*
69
- ```
70
-
71
- 已发布版本号不可覆盖。
@@ -1,54 +0,0 @@
1
- # tinet-data-foundry-cli
2
-
3
- Tinet Data Foundry 取数 CLI。包名 `tinet-data-foundry-cli`,命令 **`tdf`**(避开系统磁盘命令 `df`)。
4
-
5
- 鉴权只用环境变量,**不要**把 AccessKey / SecretKey 写入本地文件。
6
-
7
- ```bash
8
- pip install -U tinet-data-foundry-cli
9
-
10
- export DF_ENDPOINT="https://data-foundry-data-api.tinetcloud.com"
11
- export DF_AK="你的_AccessKey"
12
- export DF_SK="你的_SecretKey"
13
-
14
- tdf catalog
15
- tdf schema --dataset "DATASET_ID"
16
- tdf query \
17
- --dataset "DATASET_ID" \
18
- --fields "id" \
19
- --filter '{"op":"eq","field":"id","value":"sample"}' \
20
- --sort '[{"field":"id","direction":"DESC"}]' \
21
- --page-no 1 \
22
- --page-size 10
23
- ```
24
-
25
- 需要 Python **≥ 3.10**。若 pip 提示 scripts 目录不在 PATH,把该目录加入 PATH,或用 `python3 -m tinet_data_foundry_cli`。
26
-
27
- 公司代理 / 自签证书导致 `CERTIFICATE_VERIFY_FAILED` 时:
28
-
29
- ```bash
30
- tdf --insecure catalog
31
- tdf catalog --insecure
32
- ```
33
-
34
- | 命令 | 接口 |
35
- | --- | --- |
36
- | `tdf catalog` | `GET /data-api/v1/catalog` |
37
- | `tdf query` | `POST /data-api/v1/datasets/{id}/query` |
38
- | `tdf schema` | `GET /data-api/v1/datasets/{id}/schema` |
39
-
40
- 签名:`canonical = METHOD + "\n" + PATH + "\n" + TIMESTAMP + "\n" + NONCE + "\n" + hex(SHA256(body))`,`X-DF-Signature = Base64(HMAC-SHA256(canonical, SK))`。GET 的 body 为空字节。PATH 不含域名、不含 query。
41
-
42
- ## 发布到 PyPI
43
-
44
- 版本号改 `pyproject.toml` 与 `src/tinet_data_foundry_cli/__init__.py`。
45
-
46
- ```bash
47
- python -m pip install -U build twine
48
- python -m build
49
- python -m twine check dist/*
50
- python -m twine upload --repository testpypi dist/*
51
- python -m twine upload dist/*
52
- ```
53
-
54
- 已发布版本号不可覆盖。
@@ -1 +0,0 @@
1
- __version__ = "0.1.2"
@@ -1,71 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: tinet-data-foundry-cli
3
- Version: 0.1.2
4
- Summary: Tinet Data Foundry 取数 CLI(catalog / query / schema)
5
- Author: Tinet
6
- License: MIT
7
- Project-URL: Homepage, https://data-foundry.tinetcloud.com
8
- Project-URL: Documentation, https://data-foundry.tinetcloud.com
9
- Keywords: data-foundry,tinet,cli
10
- Classifier: Programming Language :: Python :: 3
11
- Classifier: Programming Language :: Python :: 3.10
12
- Classifier: Programming Language :: Python :: 3.11
13
- Classifier: Programming Language :: Python :: 3.12
14
- Classifier: Environment :: Console
15
- Requires-Python: >=3.10
16
- Description-Content-Type: text/markdown
17
-
18
- # tinet-data-foundry-cli
19
-
20
- Tinet Data Foundry 取数 CLI。包名 `tinet-data-foundry-cli`,命令 **`tdf`**(避开系统磁盘命令 `df`)。
21
-
22
- 鉴权只用环境变量,**不要**把 AccessKey / SecretKey 写入本地文件。
23
-
24
- ```bash
25
- pip install -U tinet-data-foundry-cli
26
-
27
- export DF_ENDPOINT="https://data-foundry-data-api.tinetcloud.com"
28
- export DF_AK="你的_AccessKey"
29
- export DF_SK="你的_SecretKey"
30
-
31
- tdf catalog
32
- tdf schema --dataset "DATASET_ID"
33
- tdf query \
34
- --dataset "DATASET_ID" \
35
- --fields "id" \
36
- --filter '{"op":"eq","field":"id","value":"sample"}' \
37
- --sort '[{"field":"id","direction":"DESC"}]' \
38
- --page-no 1 \
39
- --page-size 10
40
- ```
41
-
42
- 需要 Python **≥ 3.10**。若 pip 提示 scripts 目录不在 PATH,把该目录加入 PATH,或用 `python3 -m tinet_data_foundry_cli`。
43
-
44
- 公司代理 / 自签证书导致 `CERTIFICATE_VERIFY_FAILED` 时:
45
-
46
- ```bash
47
- tdf --insecure catalog
48
- tdf catalog --insecure
49
- ```
50
-
51
- | 命令 | 接口 |
52
- | --- | --- |
53
- | `tdf catalog` | `GET /data-api/v1/catalog` |
54
- | `tdf query` | `POST /data-api/v1/datasets/{id}/query` |
55
- | `tdf schema` | `GET /data-api/v1/datasets/{id}/schema` |
56
-
57
- 签名:`canonical = METHOD + "\n" + PATH + "\n" + TIMESTAMP + "\n" + NONCE + "\n" + hex(SHA256(body))`,`X-DF-Signature = Base64(HMAC-SHA256(canonical, SK))`。GET 的 body 为空字节。PATH 不含域名、不含 query。
58
-
59
- ## 发布到 PyPI
60
-
61
- 版本号改 `pyproject.toml` 与 `src/tinet_data_foundry_cli/__init__.py`。
62
-
63
- ```bash
64
- python -m pip install -U build twine
65
- python -m build
66
- python -m twine check dist/*
67
- python -m twine upload --repository testpypi dist/*
68
- python -m twine upload dist/*
69
- ```
70
-
71
- 已发布版本号不可覆盖。
@@ -1,26 +0,0 @@
1
- import unittest
2
- from unittest.mock import patch
3
-
4
- from tinet_data_foundry_cli.cli import build_parser, main
5
-
6
-
7
- class CliParserTest(unittest.TestCase):
8
- def test_subcommands(self):
9
- parser = build_parser()
10
- catalog = parser.parse_args(["catalog", "--format", "json"])
11
- self.assertEqual(catalog.cmd, "catalog")
12
- query = parser.parse_args(["query", "--dataset", "ds_1", "--page-no", "1", "--page-size", "10"])
13
- self.assertEqual(query.cmd, "query")
14
- catalog_k = parser.parse_args(["catalog", "--insecure"])
15
- self.assertTrue(catalog_k.insecure)
16
- catalog_before = parser.parse_args(["--insecure", "catalog"])
17
- self.assertTrue(catalog_before.insecure)
18
-
19
- def test_dry_run_catalog_does_not_need_network(self):
20
- with patch.dict("os.environ", {"DF_AK": "ak", "DF_SK": "sk" * 8}, clear=False):
21
- code = main(["--dry-run", "catalog"])
22
- self.assertEqual(code, 0)
23
-
24
-
25
- if __name__ == "__main__":
26
- unittest.main()