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.
- tinet_data_foundry_cli-0.1.5/PKG-INFO +115 -0
- tinet_data_foundry_cli-0.1.5/README.md +98 -0
- {tinet_data_foundry_cli-0.1.2 → tinet_data_foundry_cli-0.1.5}/pyproject.toml +2 -2
- tinet_data_foundry_cli-0.1.5/src/tinet_data_foundry_cli/__init__.py +1 -0
- {tinet_data_foundry_cli-0.1.2 → tinet_data_foundry_cli-0.1.5}/src/tinet_data_foundry_cli/cli.py +87 -18
- tinet_data_foundry_cli-0.1.5/src/tinet_data_foundry_cli/docs.py +202 -0
- tinet_data_foundry_cli-0.1.5/src/tinet_data_foundry_cli.egg-info/PKG-INFO +115 -0
- {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
- tinet_data_foundry_cli-0.1.5/tests/test_cli.py +95 -0
- tinet_data_foundry_cli-0.1.5/tests/test_docs.py +52 -0
- tinet_data_foundry_cli-0.1.2/PKG-INFO +0 -71
- tinet_data_foundry_cli-0.1.2/README.md +0 -54
- tinet_data_foundry_cli-0.1.2/src/tinet_data_foundry_cli/__init__.py +0 -1
- tinet_data_foundry_cli-0.1.2/src/tinet_data_foundry_cli.egg-info/PKG-INFO +0 -71
- tinet_data_foundry_cli-0.1.2/tests/test_cli.py +0 -26
- {tinet_data_foundry_cli-0.1.2 → tinet_data_foundry_cli-0.1.5}/setup.cfg +0 -0
- {tinet_data_foundry_cli-0.1.2 → tinet_data_foundry_cli-0.1.5}/src/tinet_data_foundry_cli/__main__.py +0 -0
- {tinet_data_foundry_cli-0.1.2 → tinet_data_foundry_cli-0.1.5}/src/tinet_data_foundry_cli/auth.py +0 -0
- {tinet_data_foundry_cli-0.1.2 → tinet_data_foundry_cli-0.1.5}/src/tinet_data_foundry_cli/client.py +0 -0
- {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
- {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
- {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
- {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.
|
|
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"
|
{tinet_data_foundry_cli-0.1.2 → tinet_data_foundry_cli-0.1.5}/src/tinet_data_foundry_cli/cli.py
RENAMED
|
@@ -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
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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(
|
|
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(
|
|
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()
|
|
File without changes
|
{tinet_data_foundry_cli-0.1.2 → tinet_data_foundry_cli-0.1.5}/src/tinet_data_foundry_cli/__main__.py
RENAMED
|
File without changes
|
{tinet_data_foundry_cli-0.1.2 → tinet_data_foundry_cli-0.1.5}/src/tinet_data_foundry_cli/auth.py
RENAMED
|
File without changes
|
{tinet_data_foundry_cli-0.1.2 → tinet_data_foundry_cli-0.1.5}/src/tinet_data_foundry_cli/client.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|