maxc-cli 0.1.3__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.
- {maxc_cli-0.1.3/src/maxc_cli.egg-info → maxc_cli-0.1.5}/PKG-INFO +43 -8
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/README.md +42 -7
- maxc_cli-0.1.5/scripts/regression_test.py +326 -0
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/setup.py +1 -1
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/src/maxc_cli/__init__.py +1 -1
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/src/maxc_cli/app.py +510 -232
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/src/maxc_cli/backend/odps.py +24 -29
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/src/maxc_cli/backend/query.py +66 -20
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/src/maxc_cli/cli.py +164 -34
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/src/maxc_cli/config.py +5 -0
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/src/maxc_cli/exceptions.py +40 -0
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/src/maxc_cli/helpers.py +65 -0
- maxc_cli-0.1.5/src/maxc_cli/masking.py +104 -0
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/src/maxc_cli/models.py +172 -46
- maxc_cli-0.1.5/src/maxc_cli/output.py +296 -0
- maxc_cli-0.1.5/src/maxc_cli/setting_parser.py +126 -0
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/src/maxc_cli/skills/SKILL.md +176 -7
- maxc_cli-0.1.5/src/maxc_cli/skills/nohup.out +0 -0
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/src/maxc_cli/skills/references/command-patterns.md +11 -4
- maxc_cli-0.1.5/src/maxc_cli/skills/references/maxcompute-sql-notes.md +178 -0
- maxc_cli-0.1.5/src/maxc_cli/skills/references/partition-guide.md +135 -0
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/src/maxc_cli/utils.py +9 -1
- {maxc_cli-0.1.3 → maxc_cli-0.1.5/src/maxc_cli.egg-info}/PKG-INFO +43 -8
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/src/maxc_cli.egg-info/SOURCES.txt +11 -2
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/tests/test_agent_hints_and_cli.py +14 -5
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/tests/test_agent_skill_commands_context.py +35 -9
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/tests/test_cli_mock.py +3 -3
- maxc_cli-0.1.5/tests/test_error_self_correction.py +75 -0
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/tests/test_external_auth.py +2 -0
- maxc_cli-0.1.5/tests/test_masking.py +134 -0
- maxc_cli-0.1.5/tests/test_phase1_improvements.py +568 -0
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/tests/test_query_auto_promote.py +10 -5
- maxc_cli-0.1.5/tests/test_setting_parser.py +211 -0
- maxc_cli-0.1.3/skills/use-maxc-cli/.DS_Store +0 -0
- maxc_cli-0.1.3/src/maxc_cli/output.py +0 -75
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/MANIFEST.in +0 -0
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/pyproject.toml +0 -0
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/setup.cfg +0 -0
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/src/maxc_cli/__main__.py +0 -0
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/src/maxc_cli/audit.py +0 -0
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/src/maxc_cli/auth_providers.py +0 -0
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/src/maxc_cli/backend/__init__.py +0 -0
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/src/maxc_cli/backend/auth.py +0 -0
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/src/maxc_cli/backend/catalog.py +0 -0
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/src/maxc_cli/backend/data.py +0 -0
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/src/maxc_cli/backend/job.py +0 -0
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/src/maxc_cli/backend/meta.py +0 -0
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/src/maxc_cli/cache.py +0 -0
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/src/maxc_cli/skills/agents/openai.yaml +0 -0
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/src/maxc_cli/skills/references/bootstrap-auth.md +0 -0
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/src/maxc_cli/skills/references/migrate-from-odpscmd.md +0 -0
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/src/maxc_cli/skills/references/setup-install.md +0 -0
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/src/maxc_cli/store.py +0 -0
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/src/maxc_cli.egg-info/dependency_links.txt +0 -0
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/src/maxc_cli.egg-info/entry_points.txt +0 -0
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/src/maxc_cli.egg-info/requires.txt +0 -0
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/src/maxc_cli.egg-info/top_level.txt +0 -0
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/tests/test_cache.py +0 -0
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/tests/test_catalog.py +0 -0
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/tests/test_compat.py +0 -0
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/tests/test_e2e_smoke.py +0 -0
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/tests/test_integration.py +0 -0
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/tests/test_integration_real.py +0 -0
- {maxc_cli-0.1.3 → maxc_cli-0.1.5}/tests/test_job_improvements.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: maxc-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
4
4
|
Summary: Agent-native MaxCompute CLI for external coding agents
|
|
5
5
|
Classifier: Programming Language :: Python :: 3
|
|
6
6
|
Classifier: Programming Language :: Python :: 3.8
|
|
@@ -57,7 +57,7 @@ maxc query "SELECT * FROM schema.table WHERE ds='20260415'" --json
|
|
|
57
57
|
| **cache** | `build`, `build-status`, `status`, `clear` | 元数据缓存管理 |
|
|
58
58
|
| **agent** | `context`, `skill`, `install-skill` | Agent 集成与 SKILL 安装 |
|
|
59
59
|
|
|
60
|
-
所有命令支持 `--json` 输出 Envelope v2.0
|
|
60
|
+
所有命令支持 `--json` 输出 Envelope v2.0 结构化响应。支持 `--format json|markdown|brief`(全局标志)。
|
|
61
61
|
|
|
62
62
|
## Agent 集成
|
|
63
63
|
|
|
@@ -71,10 +71,10 @@ SKILL HUB 安装 SKILL → Agent 读 SKILL.md → Agent 自己 `pip install maxc
|
|
|
71
71
|
|
|
72
72
|
```bash
|
|
73
73
|
pip install maxc-cli
|
|
74
|
-
maxc agent install-skill claude-code # 或 cursor / windsurf / codex / qwen
|
|
74
|
+
maxc agent install-skill claude-code # 或 cursor / windsurf / codex / qwen / qoder / qoderwork
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
-
`install-skill`
|
|
77
|
+
`install-skill` 从独立仓库 [maxcompute-cli-guidance](https://gitlab.alibaba-inc.com/aone-open-skill/maxcompute-cli-guidance) 获取 SKILL.md + references/ 并安装到目标平台目录。
|
|
78
78
|
|
|
79
79
|
### preflight 检查
|
|
80
80
|
|
|
@@ -99,18 +99,53 @@ maxc agent skill --json # SKILL.md 路径与 min_cli_version
|
|
|
99
99
|
"metadata": { ... },
|
|
100
100
|
"error": null | { "code": "...", "message": "...", "recovery_steps": [...] },
|
|
101
101
|
"agent_hints": {
|
|
102
|
-
"
|
|
103
|
-
|
|
102
|
+
"actions": [
|
|
103
|
+
{
|
|
104
|
+
"id": "meta.search",
|
|
105
|
+
"title": "Search tables",
|
|
106
|
+
"command": "maxc meta search <keyword> --json",
|
|
107
|
+
"executable": false,
|
|
108
|
+
"placeholders": {"keyword": "search keyword"},
|
|
109
|
+
"args_schema": {}
|
|
110
|
+
}
|
|
111
|
+
],
|
|
112
|
+
"action_ids": ["meta.search"],
|
|
113
|
+
"next_actions": ["maxc meta search <keyword> --json"],
|
|
104
114
|
"insights": [...],
|
|
105
115
|
"warnings": [...]
|
|
106
116
|
}
|
|
107
117
|
}
|
|
108
118
|
```
|
|
109
119
|
|
|
120
|
+
- `agent_hints.actions[]`: 结构化 `SuggestedAction` 对象数组(Phase 1 新增)
|
|
110
121
|
- `action_ids`:稳定 dot-notation,用于程序化路由(`meta.describe`、`job.wait`)
|
|
111
|
-
- `next_actions`:可直接 copy-paste 的 CLI
|
|
122
|
+
- `next_actions`:可直接 copy-paste 的 CLI 命令(从 `actions[]` 派生)
|
|
112
123
|
- `error.recovery_steps`:错误码对应的恢复步骤
|
|
113
124
|
|
|
125
|
+
### 输出格式
|
|
126
|
+
|
|
127
|
+
`--format` 是全局标志,适用于所有命令:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
maxc --format json meta describe my_table # 结构化 JSON(默认,等价于 --json)
|
|
131
|
+
maxc --format markdown meta describe my_table # 人类可读 markdown
|
|
132
|
+
maxc --format brief meta describe my_table # 最小化单行输出(低 token 场景)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### safety 块
|
|
136
|
+
|
|
137
|
+
`query` 和 `job` 命令的 `data` 中包含 `safety` 字段,描述安全策略决策:
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
"safety": {
|
|
141
|
+
"mode": "read_only",
|
|
142
|
+
"force": false,
|
|
143
|
+
"allowed_operations": ["SELECT"],
|
|
144
|
+
"effective_hints": {"odps.sql.read.only": "true"},
|
|
145
|
+
"policy_decision": "allowed"
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
114
149
|
详见 [`docs/ENVELOPE_SPEC.md`](docs/ENVELOPE_SPEC.md)。
|
|
115
150
|
|
|
116
151
|
## 项目结构
|
|
@@ -127,7 +162,7 @@ src/maxc_cli/
|
|
|
127
162
|
├── output.py # Rich / 纯文本渲染
|
|
128
163
|
├── auth_providers.py # AK-SK / NCS / 环境变量认证
|
|
129
164
|
├── backend/ # ODPS 后端(query / meta / catalog / data / diff 五个 mixin)
|
|
130
|
-
└── skills/
|
|
165
|
+
└── (skills/ 已迁移至独立仓库 aone-open-skill/maxcompute-cli-guidance)
|
|
131
166
|
```
|
|
132
167
|
|
|
133
168
|
## 文档
|
|
@@ -36,7 +36,7 @@ maxc query "SELECT * FROM schema.table WHERE ds='20260415'" --json
|
|
|
36
36
|
| **cache** | `build`, `build-status`, `status`, `clear` | 元数据缓存管理 |
|
|
37
37
|
| **agent** | `context`, `skill`, `install-skill` | Agent 集成与 SKILL 安装 |
|
|
38
38
|
|
|
39
|
-
所有命令支持 `--json` 输出 Envelope v2.0
|
|
39
|
+
所有命令支持 `--json` 输出 Envelope v2.0 结构化响应。支持 `--format json|markdown|brief`(全局标志)。
|
|
40
40
|
|
|
41
41
|
## Agent 集成
|
|
42
42
|
|
|
@@ -50,10 +50,10 @@ SKILL HUB 安装 SKILL → Agent 读 SKILL.md → Agent 自己 `pip install maxc
|
|
|
50
50
|
|
|
51
51
|
```bash
|
|
52
52
|
pip install maxc-cli
|
|
53
|
-
maxc agent install-skill claude-code # 或 cursor / windsurf / codex / qwen
|
|
53
|
+
maxc agent install-skill claude-code # 或 cursor / windsurf / codex / qwen / qoder / qoderwork
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
`install-skill`
|
|
56
|
+
`install-skill` 从独立仓库 [maxcompute-cli-guidance](https://gitlab.alibaba-inc.com/aone-open-skill/maxcompute-cli-guidance) 获取 SKILL.md + references/ 并安装到目标平台目录。
|
|
57
57
|
|
|
58
58
|
### preflight 检查
|
|
59
59
|
|
|
@@ -78,18 +78,53 @@ maxc agent skill --json # SKILL.md 路径与 min_cli_version
|
|
|
78
78
|
"metadata": { ... },
|
|
79
79
|
"error": null | { "code": "...", "message": "...", "recovery_steps": [...] },
|
|
80
80
|
"agent_hints": {
|
|
81
|
-
"
|
|
82
|
-
|
|
81
|
+
"actions": [
|
|
82
|
+
{
|
|
83
|
+
"id": "meta.search",
|
|
84
|
+
"title": "Search tables",
|
|
85
|
+
"command": "maxc meta search <keyword> --json",
|
|
86
|
+
"executable": false,
|
|
87
|
+
"placeholders": {"keyword": "search keyword"},
|
|
88
|
+
"args_schema": {}
|
|
89
|
+
}
|
|
90
|
+
],
|
|
91
|
+
"action_ids": ["meta.search"],
|
|
92
|
+
"next_actions": ["maxc meta search <keyword> --json"],
|
|
83
93
|
"insights": [...],
|
|
84
94
|
"warnings": [...]
|
|
85
95
|
}
|
|
86
96
|
}
|
|
87
97
|
```
|
|
88
98
|
|
|
99
|
+
- `agent_hints.actions[]`: 结构化 `SuggestedAction` 对象数组(Phase 1 新增)
|
|
89
100
|
- `action_ids`:稳定 dot-notation,用于程序化路由(`meta.describe`、`job.wait`)
|
|
90
|
-
- `next_actions`:可直接 copy-paste 的 CLI
|
|
101
|
+
- `next_actions`:可直接 copy-paste 的 CLI 命令(从 `actions[]` 派生)
|
|
91
102
|
- `error.recovery_steps`:错误码对应的恢复步骤
|
|
92
103
|
|
|
104
|
+
### 输出格式
|
|
105
|
+
|
|
106
|
+
`--format` 是全局标志,适用于所有命令:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
maxc --format json meta describe my_table # 结构化 JSON(默认,等价于 --json)
|
|
110
|
+
maxc --format markdown meta describe my_table # 人类可读 markdown
|
|
111
|
+
maxc --format brief meta describe my_table # 最小化单行输出(低 token 场景)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### safety 块
|
|
115
|
+
|
|
116
|
+
`query` 和 `job` 命令的 `data` 中包含 `safety` 字段,描述安全策略决策:
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
"safety": {
|
|
120
|
+
"mode": "read_only",
|
|
121
|
+
"force": false,
|
|
122
|
+
"allowed_operations": ["SELECT"],
|
|
123
|
+
"effective_hints": {"odps.sql.read.only": "true"},
|
|
124
|
+
"policy_decision": "allowed"
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
93
128
|
详见 [`docs/ENVELOPE_SPEC.md`](docs/ENVELOPE_SPEC.md)。
|
|
94
129
|
|
|
95
130
|
## 项目结构
|
|
@@ -106,7 +141,7 @@ src/maxc_cli/
|
|
|
106
141
|
├── output.py # Rich / 纯文本渲染
|
|
107
142
|
├── auth_providers.py # AK-SK / NCS / 环境变量认证
|
|
108
143
|
├── backend/ # ODPS 后端(query / meta / catalog / data / diff 五个 mixin)
|
|
109
|
-
└── skills/
|
|
144
|
+
└── (skills/ 已迁移至独立仓库 aone-open-skill/maxcompute-cli-guidance)
|
|
110
145
|
```
|
|
111
146
|
|
|
112
147
|
## 文档
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Pre-release regression test - runs all CLI commands against real MaxCompute backend."""
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import tempfile
|
|
7
|
+
from io import StringIO
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src"))
|
|
11
|
+
from maxc_cli.cli import run
|
|
12
|
+
|
|
13
|
+
RESULTS = []
|
|
14
|
+
TABLE = None
|
|
15
|
+
TABLE_COL = None
|
|
16
|
+
JOB_ID = None
|
|
17
|
+
|
|
18
|
+
def assert_eq(a, b): assert a == b, f"expected {b!r}, got {a!r}"
|
|
19
|
+
def assert_true(v): assert v, f"expected truthy, got {v!r}"
|
|
20
|
+
def assert_key(d, k): assert k in d, f"missing key: {k!r} in {list(d.keys())}"
|
|
21
|
+
def assert_type(v, t): assert isinstance(v, t), f"expected {t.__name__}, got {type(v).__name__}"
|
|
22
|
+
|
|
23
|
+
def run_cmd(label, argv, expect_code=0, check=None):
|
|
24
|
+
global JOB_ID
|
|
25
|
+
stdout, stderr = StringIO(), StringIO()
|
|
26
|
+
code = run(argv, cwd=Path.cwd(), stdout=stdout, stderr=stderr)
|
|
27
|
+
out = stdout.getvalue()
|
|
28
|
+
err = stderr.getvalue()
|
|
29
|
+
try:
|
|
30
|
+
data = json.loads(out) if out.strip() else {}
|
|
31
|
+
except json.JSONDecodeError:
|
|
32
|
+
data = {"_raw": out[:300]}
|
|
33
|
+
|
|
34
|
+
ok = (expect_code is None) or (code == expect_code)
|
|
35
|
+
detail = ""
|
|
36
|
+
if not ok:
|
|
37
|
+
detail = f"exit_code={code}, expected={expect_code}"
|
|
38
|
+
if err.strip():
|
|
39
|
+
detail += f" stderr={err[:200]}"
|
|
40
|
+
if ok and check:
|
|
41
|
+
try:
|
|
42
|
+
check(data)
|
|
43
|
+
except Exception as e:
|
|
44
|
+
ok = False
|
|
45
|
+
detail = str(e)
|
|
46
|
+
|
|
47
|
+
icon = "PASS" if ok else "FAIL"
|
|
48
|
+
RESULTS.append((label, icon, detail))
|
|
49
|
+
suffix = f" -- {detail}" if detail else ""
|
|
50
|
+
print(f" [{icon}] {label}{suffix}")
|
|
51
|
+
sys.stdout.flush()
|
|
52
|
+
return code, data
|
|
53
|
+
|
|
54
|
+
def pdata(d):
|
|
55
|
+
return d.get("data", {})
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ===========================================================
|
|
59
|
+
print("\n=== 1. Auth ===")
|
|
60
|
+
# ===========================================================
|
|
61
|
+
run_cmd("1.1 auth whoami", ["auth", "whoami", "--json"],
|
|
62
|
+
check=lambda d: (
|
|
63
|
+
assert_eq(d["status"], "success"),
|
|
64
|
+
assert_true(pdata(d)["identity"]["authenticated"]),
|
|
65
|
+
))
|
|
66
|
+
|
|
67
|
+
# Get test table
|
|
68
|
+
_, tdata = run_cmd("(preflight) list-tables", ["meta", "list-tables", "--json"])
|
|
69
|
+
tables = pdata(tdata).get("tables", [])
|
|
70
|
+
TABLE = tables[0]["table_name"] if tables else None
|
|
71
|
+
if not TABLE:
|
|
72
|
+
print(" [SKIP] No tables available, aborting")
|
|
73
|
+
sys.exit(1)
|
|
74
|
+
print(f" >> Test table: {TABLE}")
|
|
75
|
+
|
|
76
|
+
# Describe to get column name
|
|
77
|
+
_, ddata = run_cmd("(preflight) describe", ["meta", "describe", TABLE, "--json"])
|
|
78
|
+
cols = pdata(ddata).get("table", {}).get("schema", [])
|
|
79
|
+
TABLE_COL = cols[0]["name"] if cols else "id"
|
|
80
|
+
print(f" >> Test column: {TABLE_COL}")
|
|
81
|
+
|
|
82
|
+
run_cmd("1.2 auth can-i SELECT", ["auth", "can-i", "--table", TABLE, "--operation", "SELECT", "--json"],
|
|
83
|
+
expect_code=None, # may be 0 or 1 depending on permissions
|
|
84
|
+
check=lambda d: assert_key(pdata(d), "authorization"))
|
|
85
|
+
|
|
86
|
+
run_cmd("1.3 auth can-i --brief", ["auth", "can-i", "--table", TABLE, "--operation", "SELECT", "--brief", "--json"],
|
|
87
|
+
expect_code=None, # may be 0 or 1
|
|
88
|
+
check=lambda d: assert_true(d["status"] in ("success", "failure")))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ===========================================================
|
|
92
|
+
print("\n=== 2. Meta ===")
|
|
93
|
+
# ===========================================================
|
|
94
|
+
run_cmd("2.1 meta list-tables", ["meta", "list-tables", "--json"],
|
|
95
|
+
check=lambda d: assert_true(len(pdata(d).get("tables", [])) > 0))
|
|
96
|
+
|
|
97
|
+
run_cmd("2.2 meta describe", ["meta", "describe", TABLE, "--json"],
|
|
98
|
+
check=lambda d: (
|
|
99
|
+
assert_eq(pdata(d)["table"]["table_name"], TABLE),
|
|
100
|
+
assert_key(pdata(d)["table"], "schema"),
|
|
101
|
+
))
|
|
102
|
+
|
|
103
|
+
run_cmd("2.3 meta describe --full", ["meta", "describe", TABLE, "--full", "--json"],
|
|
104
|
+
check=lambda d: assert_eq(d["status"], "success"))
|
|
105
|
+
|
|
106
|
+
run_cmd("2.4 meta search", ["meta", "search", "test", "--json"],
|
|
107
|
+
check=lambda d: assert_key(pdata(d)["search"], "matches"))
|
|
108
|
+
|
|
109
|
+
run_cmd("2.5 meta search-columns", ["meta", "search-columns", "id", "--json"],
|
|
110
|
+
check=lambda d: assert_key(pdata(d)["search"], "matches"))
|
|
111
|
+
|
|
112
|
+
run_cmd("2.6 meta partitions", ["meta", "partitions", TABLE, "--json"],
|
|
113
|
+
check=lambda d: assert_key(pdata(d), "partitions"))
|
|
114
|
+
|
|
115
|
+
run_cmd("2.7 meta latest-partition", ["meta", "latest-partition", TABLE, "--json"],
|
|
116
|
+
check=lambda d: assert_key(pdata(d)["partition"], "has_partitions"))
|
|
117
|
+
|
|
118
|
+
run_cmd("2.8 meta freshness", ["meta", "freshness", TABLE, "--json"],
|
|
119
|
+
check=lambda d: assert_key(pdata(d)["freshness"], "freshness_status"))
|
|
120
|
+
|
|
121
|
+
run_cmd("2.9 meta list-projects", ["meta", "list-projects", "--json"],
|
|
122
|
+
check=lambda d: assert_type(pdata(d)["projects"], list))
|
|
123
|
+
|
|
124
|
+
run_cmd("2.10 meta list-schemas", ["meta", "list-schemas", "--json"],
|
|
125
|
+
expect_code=None, # may fail if catalog not available
|
|
126
|
+
check=lambda d: assert_true(d["status"] in ("success", "failure")))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# ===========================================================
|
|
130
|
+
print("\n=== 3. Meta Semantic ===")
|
|
131
|
+
# ===========================================================
|
|
132
|
+
run_cmd("3.1 meta semantic set", ["meta", "semantic", "set", TABLE, "--desc", "regression test desc", "--json"],
|
|
133
|
+
check=lambda d: assert_eq(d["status"], "success"))
|
|
134
|
+
|
|
135
|
+
run_cmd("3.2 meta semantic get", ["meta", "semantic", "get", TABLE, "--json"],
|
|
136
|
+
check=lambda d: assert_eq(d["status"], "success"))
|
|
137
|
+
|
|
138
|
+
run_cmd("3.3 meta semantic list-missing", ["meta", "semantic", "list-missing", "--json"],
|
|
139
|
+
check=lambda d: assert_eq(d["status"], "success"))
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ===========================================================
|
|
143
|
+
print("\n=== 4. Query ===")
|
|
144
|
+
# ===========================================================
|
|
145
|
+
run_cmd("4.1 query simple", ["query", "SELECT 1 AS c1, 'test' AS c2", "--json", "--force"],
|
|
146
|
+
check=lambda d: (
|
|
147
|
+
assert_eq(d["status"], "success"),
|
|
148
|
+
assert_true(len(pdata(d)["result"]["rows"]) == 1),
|
|
149
|
+
))
|
|
150
|
+
|
|
151
|
+
run_cmd("4.2 query cost", ["query", "cost", "SELECT 1 AS c1", "--json", "--force"],
|
|
152
|
+
check=lambda d: assert_key(pdata(d)["analysis"], "cost_model"))
|
|
153
|
+
|
|
154
|
+
run_cmd("4.3 query explain", ["query", "explain", "SELECT 1 AS c1", "--json", "--force"],
|
|
155
|
+
check=lambda d: assert_eq(pdata(d)["analysis"]["analysis_mode"], "explain"))
|
|
156
|
+
|
|
157
|
+
run_cmd("4.4 query pagination",
|
|
158
|
+
["query", "SELECT 1 AS id UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5",
|
|
159
|
+
"--page-size", "2", "--json", "--force"],
|
|
160
|
+
check=lambda d: (
|
|
161
|
+
assert_true(pdata(d)["pagination"]["has_more"]),
|
|
162
|
+
assert_true(pdata(d)["pagination"]["next_cursor"] is not None),
|
|
163
|
+
))
|
|
164
|
+
|
|
165
|
+
run_cmd("4.5 query dry-run", ["query", "SELECT 1", "--dry-run", "--json", "--force"],
|
|
166
|
+
check=lambda d: assert_eq(pdata(d)["result"]["returned_rows"], 0))
|
|
167
|
+
|
|
168
|
+
run_cmd("4.6 query wait=0", ["query", "SELECT 1 AS async_test", "--wait", "0", "--json", "--force"],
|
|
169
|
+
check=lambda d: (
|
|
170
|
+
assert_eq(d["status"], "pending"),
|
|
171
|
+
assert_key(pdata(d), "job"),
|
|
172
|
+
))
|
|
173
|
+
|
|
174
|
+
tmpfile = Path(tempfile.mktemp(suffix=".csv"))
|
|
175
|
+
run_cmd("4.7 query --output csv",
|
|
176
|
+
["query", "SELECT 1 AS a, 2 AS b", "--json", "--force", "--output", str(tmpfile), "--output-format", "csv"],
|
|
177
|
+
check=lambda d: assert_eq(d["status"], "success"))
|
|
178
|
+
if tmpfile.exists():
|
|
179
|
+
print(f" CSV file created: {tmpfile.stat().st_size} bytes")
|
|
180
|
+
tmpfile.unlink()
|
|
181
|
+
else:
|
|
182
|
+
RESULTS.append(("4.7b csv file exists", "FAIL", "file not created"))
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# ===========================================================
|
|
186
|
+
print("\n=== 5. Query: New Features ===")
|
|
187
|
+
# ===========================================================
|
|
188
|
+
run_cmd("5.1 masking (phone/email/password)",
|
|
189
|
+
["query", "SELECT 1 as id, '13812345678' as phone, 'alice@example.com' as email, 'secret' as password",
|
|
190
|
+
"--force", "--json"],
|
|
191
|
+
check=lambda d: (
|
|
192
|
+
assert_eq(pdata(d)["result"]["rows"][0]["phone"], "138****5678"),
|
|
193
|
+
assert_eq(pdata(d)["result"]["rows"][0]["email"], "a***@example.com"),
|
|
194
|
+
assert_eq(pdata(d)["result"]["rows"][0]["password"], "******"),
|
|
195
|
+
assert_true(any("masked" in w.lower() for w in d.get("agent_hints", {}).get("warnings", []))),
|
|
196
|
+
))
|
|
197
|
+
|
|
198
|
+
run_cmd("5.2 LIMIT truncation warning",
|
|
199
|
+
["query", "SELECT t.* FROM (SELECT 1 as id UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5) t",
|
|
200
|
+
"--max-rows", "2", "--force", "--json"],
|
|
201
|
+
check=lambda d: (
|
|
202
|
+
assert_true(pdata(d)["pagination"]["has_more"]),
|
|
203
|
+
assert_true(any("truncated" in w.lower() for w in d.get("agent_hints", {}).get("warnings", []))),
|
|
204
|
+
))
|
|
205
|
+
|
|
206
|
+
run_cmd("5.3 error self-correction (wrong column)",
|
|
207
|
+
["query", f"SELECT nonexistent_col FROM {TABLE} LIMIT 5", "--force", "--json"],
|
|
208
|
+
expect_code=None, # SQL_ERROR(1) or other
|
|
209
|
+
check=lambda d: (
|
|
210
|
+
assert_true(d["status"] == "failure"),
|
|
211
|
+
assert_key(d.get("data", {}), "schema_context"),
|
|
212
|
+
))
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# ===========================================================
|
|
216
|
+
print("\n=== 6. Job ===")
|
|
217
|
+
# ===========================================================
|
|
218
|
+
_, jsubmit = run_cmd("6.1 job submit", ["job", "submit", "SELECT 1 AS job_test", "--json", "--force"],
|
|
219
|
+
check=lambda d: assert_key(pdata(d), "job_id"))
|
|
220
|
+
JOB_ID = pdata(jsubmit).get("job_id")
|
|
221
|
+
if JOB_ID:
|
|
222
|
+
print(f" job_id: {JOB_ID}")
|
|
223
|
+
|
|
224
|
+
run_cmd("6.2 job status", ["job", "status", JOB_ID, "--json"],
|
|
225
|
+
check=lambda d: assert_key(pdata(d).get("job", {}), "stage"))
|
|
226
|
+
|
|
227
|
+
run_cmd("6.3 job wait", ["job", "wait", JOB_ID, "--json"],
|
|
228
|
+
check=lambda d: assert_true(d["status"] in ("success", "failure")))
|
|
229
|
+
|
|
230
|
+
run_cmd("6.4 job result", ["job", "result", JOB_ID, "--json"],
|
|
231
|
+
check=lambda d: assert_key(pdata(d).get("result", {}), "rows"))
|
|
232
|
+
|
|
233
|
+
run_cmd("6.5 job diagnose", ["job", "diagnose", JOB_ID, "--json"],
|
|
234
|
+
check=lambda d: assert_key(pdata(d).get("diagnosis", {}), "diagnosis_category"))
|
|
235
|
+
else:
|
|
236
|
+
for label in ["6.2 job status", "6.3 job wait", "6.4 job result", "6.5 job diagnose"]:
|
|
237
|
+
RESULTS.append((label, "SKIP", "no job_id"))
|
|
238
|
+
|
|
239
|
+
run_cmd("6.6 job list", ["job", "list", "--json"],
|
|
240
|
+
check=lambda d: assert_type(pdata(d).get("jobs", None), list))
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# ===========================================================
|
|
244
|
+
print("\n=== 7. Data ===")
|
|
245
|
+
# ===========================================================
|
|
246
|
+
run_cmd("7.1 data sample", ["data", "sample", TABLE, "--rows", "3", "--json"],
|
|
247
|
+
expect_code=None, # may fail if table is partitioned
|
|
248
|
+
check=lambda d: assert_true(d["status"] in ("success", "failure")))
|
|
249
|
+
|
|
250
|
+
run_cmd("7.2 data profile", ["data", "profile", TABLE, "--json"],
|
|
251
|
+
expect_code=None, # may fail if table is partitioned
|
|
252
|
+
check=lambda d: assert_true(d["status"] in ("success", "failure")))
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
# ===========================================================
|
|
256
|
+
print("\n=== 8. Diff ===")
|
|
257
|
+
# ===========================================================
|
|
258
|
+
run_cmd("8.1 diff schema self", ["diff", "schema", TABLE, TABLE, "--json"],
|
|
259
|
+
check=lambda d: assert_eq(pdata(d)["diff"]["compatible"], True))
|
|
260
|
+
|
|
261
|
+
run_cmd("8.2 diff partition self", ["diff", "partition", TABLE, TABLE, "--json"],
|
|
262
|
+
check=lambda d: assert_key(pdata(d)["diff"], "summary"))
|
|
263
|
+
|
|
264
|
+
run_cmd("8.3 diff data self", ["diff", "data", TABLE, TABLE, "--keys", TABLE_COL, "--rows", "5", "--json"],
|
|
265
|
+
expect_code=None, # may fail if table is partitioned
|
|
266
|
+
check=lambda d: assert_true(d["status"] in ("success", "failure")))
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
# ===========================================================
|
|
270
|
+
print("\n=== 9. Session ===")
|
|
271
|
+
# ===========================================================
|
|
272
|
+
run_cmd("9.1 session show", ["session", "show", "--json"],
|
|
273
|
+
check=lambda d: assert_key(pdata(d), "project"))
|
|
274
|
+
|
|
275
|
+
run_cmd("9.2 session set", ["session", "set", "--project", "meta_dev", "--json"],
|
|
276
|
+
check=lambda d: assert_eq(d["status"], "success"))
|
|
277
|
+
|
|
278
|
+
run_cmd("9.3 session unset", ["session", "unset", "--json"],
|
|
279
|
+
check=lambda d: assert_eq(d["status"], "success"))
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
# ===========================================================
|
|
283
|
+
print("\n=== 10. Agent ===")
|
|
284
|
+
# ===========================================================
|
|
285
|
+
run_cmd("10.1 agent context", ["agent", "context", "--json"],
|
|
286
|
+
check=lambda d: assert_key(pdata(d).get("context", {}), "project"))
|
|
287
|
+
|
|
288
|
+
run_cmd("10.2 agent skill", ["agent", "skill", "--json"],
|
|
289
|
+
check=lambda d: assert_eq(d["status"], "success"))
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
# ===========================================================
|
|
293
|
+
print("\n=== 11. Cache ===")
|
|
294
|
+
# ===========================================================
|
|
295
|
+
run_cmd("11.1 cache status", ["cache", "status", "--json"],
|
|
296
|
+
check=lambda d: assert_key(pdata(d), "table_count"))
|
|
297
|
+
|
|
298
|
+
run_cmd("11.2 cache clear", ["cache", "clear", "--json"],
|
|
299
|
+
check=lambda d: assert_key(pdata(d), "deleted_tables"))
|
|
300
|
+
|
|
301
|
+
run_cmd("11.3 cache build", ["cache", "build", "--json"],
|
|
302
|
+
check=lambda d: assert_key(pdata(d), "cached_tables"))
|
|
303
|
+
|
|
304
|
+
run_cmd("11.4 cache build-status", ["cache", "build-status", "--json"],
|
|
305
|
+
check=lambda d: assert_eq(d["status"], "success"))
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
# ===========================================================
|
|
309
|
+
# SUMMARY
|
|
310
|
+
# ===========================================================
|
|
311
|
+
print("\n" + "=" * 60)
|
|
312
|
+
print("REGRESSION TEST SUMMARY")
|
|
313
|
+
print("=" * 60)
|
|
314
|
+
passed = sum(1 for _, s, _ in RESULTS if s == "PASS")
|
|
315
|
+
failed = sum(1 for _, s, _ in RESULTS if s == "FAIL")
|
|
316
|
+
skipped = sum(1 for _, s, _ in RESULTS if s == "SKIP")
|
|
317
|
+
|
|
318
|
+
if failed:
|
|
319
|
+
print(f"\nFAILED ({failed}):")
|
|
320
|
+
for label, status, detail in RESULTS:
|
|
321
|
+
if status == "FAIL":
|
|
322
|
+
print(f" - {label}: {detail}")
|
|
323
|
+
|
|
324
|
+
print(f"\nTotal: {passed} passed, {failed} failed, {skipped} skipped / {len(RESULTS)} tests")
|
|
325
|
+
print("=" * 60)
|
|
326
|
+
sys.exit(1 if failed else 0)
|
|
@@ -9,7 +9,7 @@ README = ROOT / "README.md"
|
|
|
9
9
|
|
|
10
10
|
setup(
|
|
11
11
|
name="maxc-cli",
|
|
12
|
-
version="0.1.
|
|
12
|
+
version="0.1.5",
|
|
13
13
|
description="Agent-native MaxCompute CLI for external coding agents",
|
|
14
14
|
long_description=README.read_text(encoding="utf-8"),
|
|
15
15
|
long_description_content_type="text/markdown",
|