maxc-cli 0.1.0__py3-none-any.whl
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/__init__.py +5 -0
- maxc_cli/__main__.py +6 -0
- maxc_cli/app.py +3406 -0
- maxc_cli/audit.py +18 -0
- maxc_cli/auth_providers.py +471 -0
- maxc_cli/backend/__init__.py +8 -0
- maxc_cli/backend/auth.py +144 -0
- maxc_cli/backend/data.py +87 -0
- maxc_cli/backend/job.py +304 -0
- maxc_cli/backend/meta.py +312 -0
- maxc_cli/backend/odps.py +130 -0
- maxc_cli/backend/query.py +148 -0
- maxc_cli/cache.py +662 -0
- maxc_cli/cli.py +1274 -0
- maxc_cli/config.py +406 -0
- maxc_cli/exceptions.py +99 -0
- maxc_cli/helpers.py +964 -0
- maxc_cli/models.py +533 -0
- maxc_cli/output.py +75 -0
- maxc_cli/store.py +123 -0
- maxc_cli/utils.py +136 -0
- maxc_cli-0.1.0.dist-info/METADATA +220 -0
- maxc_cli-0.1.0.dist-info/RECORD +26 -0
- maxc_cli-0.1.0.dist-info/WHEEL +5 -0
- maxc_cli-0.1.0.dist-info/entry_points.txt +2 -0
- maxc_cli-0.1.0.dist-info/top_level.txt +1 -0
maxc_cli/store.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
|
|
2
|
+
import json
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Generator
|
|
6
|
+
from uuid import uuid4
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
import fcntl
|
|
10
|
+
_HAS_FCNTL = True
|
|
11
|
+
except ImportError:
|
|
12
|
+
_HAS_FCNTL = False
|
|
13
|
+
|
|
14
|
+
from .exceptions import NotFoundError
|
|
15
|
+
from .utils import now_utc_iso
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class JobStore:
|
|
19
|
+
def __init__(self, state_dir: 'Path') -> 'None':
|
|
20
|
+
self.state_dir = state_dir
|
|
21
|
+
self.state_dir.mkdir(parents=True, exist_ok=True)
|
|
22
|
+
self.path = self.state_dir / "jobs.json"
|
|
23
|
+
|
|
24
|
+
@contextmanager
|
|
25
|
+
def _locked(self, exclusive: bool = False) -> 'Generator[None, None, None]':
|
|
26
|
+
"""Acquire an advisory file lock (shared for reads, exclusive for writes).
|
|
27
|
+
|
|
28
|
+
Falls back to a no-op on platforms without fcntl (e.g. Windows).
|
|
29
|
+
"""
|
|
30
|
+
if not _HAS_FCNTL:
|
|
31
|
+
yield
|
|
32
|
+
return
|
|
33
|
+
lock_path = self.path.with_suffix(".lock")
|
|
34
|
+
lock_path.touch(exist_ok=True)
|
|
35
|
+
fd = lock_path.open("r")
|
|
36
|
+
try:
|
|
37
|
+
fcntl.flock(fd, fcntl.LOCK_EX if exclusive else fcntl.LOCK_SH)
|
|
38
|
+
yield
|
|
39
|
+
finally:
|
|
40
|
+
fcntl.flock(fd, fcntl.LOCK_UN)
|
|
41
|
+
fd.close()
|
|
42
|
+
|
|
43
|
+
def create_job(
|
|
44
|
+
self,
|
|
45
|
+
*,
|
|
46
|
+
sql: 'str',
|
|
47
|
+
project: 'str',
|
|
48
|
+
result: 'dict[str, Any]',
|
|
49
|
+
idempotency_key: 'str | None' = None,
|
|
50
|
+
) -> 'dict[str, Any]':
|
|
51
|
+
with self._locked(exclusive=True):
|
|
52
|
+
payload = self._load()
|
|
53
|
+
if idempotency_key:
|
|
54
|
+
existing_id = payload["idempotency"].get(idempotency_key)
|
|
55
|
+
if existing_id:
|
|
56
|
+
return payload["jobs"][existing_id]
|
|
57
|
+
|
|
58
|
+
job_id = f"job_{uuid4().hex[:10]}"
|
|
59
|
+
now = now_utc_iso()
|
|
60
|
+
job = {
|
|
61
|
+
"job_id": job_id,
|
|
62
|
+
"status": "pending",
|
|
63
|
+
"sql": sql,
|
|
64
|
+
"project": project,
|
|
65
|
+
"progress": 0,
|
|
66
|
+
"submitted_at": now,
|
|
67
|
+
"updated_at": now,
|
|
68
|
+
"result": result,
|
|
69
|
+
"idempotency_key": idempotency_key,
|
|
70
|
+
}
|
|
71
|
+
payload["jobs"][job_id] = job
|
|
72
|
+
if idempotency_key:
|
|
73
|
+
payload["idempotency"][idempotency_key] = job_id
|
|
74
|
+
self._save(payload)
|
|
75
|
+
return job
|
|
76
|
+
|
|
77
|
+
def get_job(self, job_id: 'str') -> 'dict[str, Any]':
|
|
78
|
+
with self._locked():
|
|
79
|
+
payload = self._load()
|
|
80
|
+
try:
|
|
81
|
+
return payload["jobs"][job_id]
|
|
82
|
+
except KeyError as exc:
|
|
83
|
+
raise NotFoundError(
|
|
84
|
+
f"Job not found: {job_id}",
|
|
85
|
+
suggestion="Run `maxc job list` to inspect available jobs.",
|
|
86
|
+
) from exc
|
|
87
|
+
|
|
88
|
+
def list_jobs(self) -> 'list[dict[str, Any]]':
|
|
89
|
+
with self._locked():
|
|
90
|
+
payload = self._load()
|
|
91
|
+
jobs = list(payload["jobs"].values())
|
|
92
|
+
jobs.sort(key=lambda item: item["submitted_at"], reverse=True)
|
|
93
|
+
return jobs
|
|
94
|
+
|
|
95
|
+
def update_job(self, job_id: 'str', **changes: 'Any') -> 'dict[str, Any]':
|
|
96
|
+
with self._locked(exclusive=True):
|
|
97
|
+
payload = self._load()
|
|
98
|
+
try:
|
|
99
|
+
job = payload["jobs"][job_id]
|
|
100
|
+
except KeyError as exc:
|
|
101
|
+
raise NotFoundError(
|
|
102
|
+
f"Job not found: {job_id}",
|
|
103
|
+
suggestion="Run `maxc job list` to inspect available jobs.",
|
|
104
|
+
) from exc
|
|
105
|
+
job.update(changes)
|
|
106
|
+
job["updated_at"] = now_utc_iso()
|
|
107
|
+
payload["jobs"][job_id] = job
|
|
108
|
+
self._save(payload)
|
|
109
|
+
return job
|
|
110
|
+
|
|
111
|
+
def _load(self) -> 'dict[str, Any]':
|
|
112
|
+
if not self.path.exists():
|
|
113
|
+
return {"jobs": {}, "idempotency": {}}
|
|
114
|
+
try:
|
|
115
|
+
return json.loads(self.path.read_text(encoding="utf-8"))
|
|
116
|
+
except (json.JSONDecodeError, ValueError):
|
|
117
|
+
return {"jobs": {}, "idempotency": {}}
|
|
118
|
+
|
|
119
|
+
def _save(self, payload: 'dict[str, Any]') -> 'None':
|
|
120
|
+
self.path.write_text(
|
|
121
|
+
json.dumps(payload, ensure_ascii=False, indent=2),
|
|
122
|
+
encoding="utf-8",
|
|
123
|
+
)
|
maxc_cli/utils.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
|
|
2
|
+
import base64
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .exceptions import ValidationError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
SQL_COMMENT_RE = re.compile(r"/\*.*?\*/|--[^\n]*", re.DOTALL)
|
|
13
|
+
TABLE_NAME_RE = re.compile(
|
|
14
|
+
r"(?i)\b(?:from|join|into|update|table)\s+([a-zA-Z_][\w.]*)"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def now_utc_iso() -> 'str':
|
|
19
|
+
return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def deep_merge(base: 'dict[str, Any]', override: 'dict[str, Any]') -> 'dict[str, Any]':
|
|
23
|
+
merged = dict(base)
|
|
24
|
+
for key, value in override.items():
|
|
25
|
+
if isinstance(value, dict) and isinstance(merged.get(key), dict):
|
|
26
|
+
merged[key] = deep_merge(merged[key], value)
|
|
27
|
+
else:
|
|
28
|
+
merged[key] = value
|
|
29
|
+
return merged
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def resolve_path(raw_path: 'str | None', *, base_dir: 'Path') -> 'Path':
|
|
33
|
+
if not raw_path:
|
|
34
|
+
raise ValidationError("Configuration path cannot be empty.")
|
|
35
|
+
path = Path(raw_path).expanduser()
|
|
36
|
+
if not path.is_absolute():
|
|
37
|
+
path = (base_dir / path).resolve()
|
|
38
|
+
return path
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def normalize_sql(sql: 'str') -> 'str':
|
|
42
|
+
stripped = SQL_COMMENT_RE.sub(" ", sql)
|
|
43
|
+
return " ".join(stripped.strip().split())
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def detect_operation(sql: 'str') -> 'str':
|
|
47
|
+
normalized = normalize_sql(sql)
|
|
48
|
+
match = re.match(r"(?i)^([a-z]+)", normalized)
|
|
49
|
+
return match.group(1).upper() if match else "UNKNOWN"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def extract_table_names(sql: 'str') -> 'list[str]':
|
|
53
|
+
normalized = normalize_sql(sql)
|
|
54
|
+
return list(dict.fromkeys(TABLE_NAME_RE.findall(normalized)))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def parse_select_projection(sql: 'str') -> 'list[str]':
|
|
58
|
+
normalized = normalize_sql(sql)
|
|
59
|
+
match = re.search(r"(?is)^select\s+(.*?)\s+from\b", normalized)
|
|
60
|
+
if not match:
|
|
61
|
+
match = re.search(r"(?is)^select\s+(.*)$", normalized)
|
|
62
|
+
if not match:
|
|
63
|
+
return []
|
|
64
|
+
projection = match.group(1).strip()
|
|
65
|
+
if projection == "*":
|
|
66
|
+
return ["*"]
|
|
67
|
+
return [part.strip() for part in projection.split(",") if part.strip()]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def projection_alias(expression: 'str', fallback_index: 'int') -> 'str':
|
|
71
|
+
alias_match = re.search(r"(?i)\bas\s+([a-zA-Z_][\w]*)$", expression)
|
|
72
|
+
if alias_match:
|
|
73
|
+
return alias_match.group(1)
|
|
74
|
+
bare = expression.split(".")[-1].strip()
|
|
75
|
+
if bare == expression and "(" in expression:
|
|
76
|
+
return f"_c{fallback_index}"
|
|
77
|
+
return bare
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def encode_cursor(offset: 'int', session_id: 'int | None' = None) -> 'str':
|
|
81
|
+
"""Encode cursor with short keys: s=session_id, o=offset."""
|
|
82
|
+
payload: 'dict[str, int]' = {"o": offset}
|
|
83
|
+
if session_id is not None:
|
|
84
|
+
payload["s"] = session_id
|
|
85
|
+
return base64.urlsafe_b64encode(json.dumps(payload, separators=(",", ":")).encode("utf-8")).decode("utf-8")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def decode_cursor(cursor: 'str | None') -> 'tuple[int, int | None]':
|
|
89
|
+
"""Decode a cursor and return (offset, session_id)."""
|
|
90
|
+
if not cursor:
|
|
91
|
+
return 0, None
|
|
92
|
+
try:
|
|
93
|
+
payload = base64.urlsafe_b64decode(cursor.encode("utf-8")).decode("utf-8")
|
|
94
|
+
value = json.loads(payload)
|
|
95
|
+
except Exception as exc:
|
|
96
|
+
raise ValidationError(
|
|
97
|
+
"The cursor could not be parsed.",
|
|
98
|
+
suggestion="Use the `next_cursor` returned by the previous response.",
|
|
99
|
+
) from exc
|
|
100
|
+
offset = value.get("o")
|
|
101
|
+
if not isinstance(offset, int) or offset < 0:
|
|
102
|
+
raise ValidationError(
|
|
103
|
+
"The cursor contains an invalid offset.",
|
|
104
|
+
suggestion="Use the `next_cursor` returned by the previous response.",
|
|
105
|
+
)
|
|
106
|
+
session_id = value.get("s")
|
|
107
|
+
return offset, session_id
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def read_sql_input(
|
|
111
|
+
sql_parts: 'list[str]',
|
|
112
|
+
*,
|
|
113
|
+
file_path: 'str | None',
|
|
114
|
+
use_stdin: 'bool',
|
|
115
|
+
stdin_text: 'str | None',
|
|
116
|
+
) -> 'str':
|
|
117
|
+
provided_sources = sum(bool(item) for item in [sql_parts, file_path, use_stdin])
|
|
118
|
+
if provided_sources == 0:
|
|
119
|
+
raise ValidationError("Provide SQL via inline text, `--file`, or `--stdin`.")
|
|
120
|
+
if provided_sources > 1:
|
|
121
|
+
raise ValidationError("SQL input must come from exactly one source: inline text, `--file`, or `--stdin`.")
|
|
122
|
+
|
|
123
|
+
if sql_parts:
|
|
124
|
+
return " ".join(sql_parts).strip()
|
|
125
|
+
if file_path:
|
|
126
|
+
return Path(file_path).read_text(encoding="utf-8").strip()
|
|
127
|
+
if use_stdin:
|
|
128
|
+
content = (stdin_text or "").strip()
|
|
129
|
+
if not content:
|
|
130
|
+
raise ValidationError("No SQL was read from stdin.")
|
|
131
|
+
return content
|
|
132
|
+
raise ValidationError("Unable to resolve SQL input.")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def short_json(value: 'Any') -> 'str':
|
|
136
|
+
return json.dumps(value, ensure_ascii=False, sort_keys=True)
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: maxc-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Agent-native MaxCompute CLI for external coding agents
|
|
5
|
+
Classifier: Programming Language :: Python :: 3
|
|
6
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
7
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Requires-Python: >=3.8,<3.13
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: PyYAML>=5.4
|
|
14
|
+
Requires-Dist: pyodps
|
|
15
|
+
Dynamic: classifier
|
|
16
|
+
Dynamic: description
|
|
17
|
+
Dynamic: description-content-type
|
|
18
|
+
Dynamic: requires-dist
|
|
19
|
+
Dynamic: requires-python
|
|
20
|
+
Dynamic: summary
|
|
21
|
+
|
|
22
|
+
# maxc-cli
|
|
23
|
+
|
|
24
|
+
`maxc-cli` 是一个面向外部 Agent 的 MaxCompute 工具层。它不是 Agent 本身,而是给 Codex、Claude Code、Cursor 或自研 Agent 调用的结构化 CLI。
|
|
25
|
+
|
|
26
|
+
当前工作树以真实 MaxCompute 为目标。连接信息可以来自环境变量,也可以通过 `maxc auth login` 持久化到本地配置文件。缺少认证时,CLI 会返回结构化引导信息,不再回退到运行时 mock catalog。
|
|
27
|
+
|
|
28
|
+
## 文档入口
|
|
29
|
+
|
|
30
|
+
- `docs/design.md`
|
|
31
|
+
产品定位、命令体系和 skill/source 布局
|
|
32
|
+
- `docs/implementation.md`
|
|
33
|
+
当前代码的真实行为和输出契约
|
|
34
|
+
- `docs/product-positioning.md`
|
|
35
|
+
为什么当前应先把 `maxc-cli` 做成工具层
|
|
36
|
+
- `docs/roadmap.md`
|
|
37
|
+
当前路线图和发布后续项
|
|
38
|
+
- `skills/use-maxc-cli/`
|
|
39
|
+
仓库内 versioned 的 Codex skill source
|
|
40
|
+
|
|
41
|
+
## 当前能力
|
|
42
|
+
|
|
43
|
+
- 统一的 Agent-Native JSON envelope
|
|
44
|
+
- `auth / session / query / job / meta / data / diff / cache / agent context`
|
|
45
|
+
- `auth login`、`auth whoami`、`auth can-i`
|
|
46
|
+
- `session set/show/unset`
|
|
47
|
+
- `query cost`、`query explain`、分页 `--page-size` / `--cursor`
|
|
48
|
+
- `meta search-columns`、richer `meta describe`、`meta latest-partition`、`meta freshness`
|
|
49
|
+
- `meta lineage` 对真实 backend 明确返回 `supported=false` 占位契约
|
|
50
|
+
- `data sample --partition --columns --rows`
|
|
51
|
+
- `data profile --partition`
|
|
52
|
+
- `diff schema`、`diff partition`、`diff data`
|
|
53
|
+
- SQLite 本地缓存、结构化审计日志、语义元数据缓存
|
|
54
|
+
- 仓库内置 `skills/use-maxc-cli/` skill source 与同步脚本 `scripts/sync_codex_skill.py`
|
|
55
|
+
|
|
56
|
+
## 安装
|
|
57
|
+
|
|
58
|
+
仓库内开发安装:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
python -m pip install -e .
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
发布后安装:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
python -m pip install maxc-cli
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
当前打包元数据支持 Python `3.6` 到 `3.12`。
|
|
71
|
+
|
|
72
|
+
基础依赖已经包含:
|
|
73
|
+
|
|
74
|
+
- `pyodps`
|
|
75
|
+
- `PyYAML`
|
|
76
|
+
- Python 3.6 下的 `dataclasses` backport
|
|
77
|
+
|
|
78
|
+
按需依赖:
|
|
79
|
+
|
|
80
|
+
- `pandas`
|
|
81
|
+
某些包含 TIMESTAMP-like 类型的结果集读取路径可能需要它,但它不是安装 `maxc-cli` 的直接前置依赖
|
|
82
|
+
|
|
83
|
+
## Codex Skill
|
|
84
|
+
|
|
85
|
+
仓库内 `skills/use-maxc-cli/` 是 canonical source。将它同步到本机 Codex skill 目录:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
python scripts/sync_codex_skill.py
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
默认目标目录是:
|
|
92
|
+
|
|
93
|
+
```text
|
|
94
|
+
~/.codex/skills/use-maxc-cli
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## 登录与 Bootstrap
|
|
98
|
+
|
|
99
|
+
建议的最短接入路径:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
maxc auth whoami --json
|
|
103
|
+
maxc auth login --from-env --json
|
|
104
|
+
maxc auth whoami --json
|
|
105
|
+
maxc cache build --json
|
|
106
|
+
maxc meta list-tables --json
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
如果当前 shell 已经有 MaxCompute 环境变量,可以直接持久化:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
maxc auth login --from-env --json
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
显式传参登录:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
maxc auth login \
|
|
119
|
+
--access-id "<access_key_id>" \
|
|
120
|
+
--secret-access-key "<access_key_secret>" \
|
|
121
|
+
--project "<project>" \
|
|
122
|
+
--endpoint "http://service.<region>.maxcompute.aliyun.com/api" \
|
|
123
|
+
--region "<region>" \
|
|
124
|
+
--json
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`auth whoami --json` 的重点字段在 `data.identity`:
|
|
128
|
+
|
|
129
|
+
- `authenticated`
|
|
130
|
+
- `configured`
|
|
131
|
+
- `validation_status`
|
|
132
|
+
- `identity_source`
|
|
133
|
+
- `project`
|
|
134
|
+
|
|
135
|
+
如果 `authenticated=false`,继续查看 `data.auth_options` 获取推荐登录动作。
|
|
136
|
+
|
|
137
|
+
默认配置发现顺序:
|
|
138
|
+
|
|
139
|
+
```text
|
|
140
|
+
~/.maxc/config.yaml
|
|
141
|
+
./.maxc/config.yaml
|
|
142
|
+
./.maxc.yaml
|
|
143
|
+
./.maxc
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
项目和 schema 的会话覆盖保存在:
|
|
147
|
+
|
|
148
|
+
```text
|
|
149
|
+
~/.maxc/session_override.yaml
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## 快速运行
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
maxc auth whoami --json
|
|
156
|
+
maxc agent context --json
|
|
157
|
+
maxc session show --json
|
|
158
|
+
maxc cache build --json
|
|
159
|
+
maxc meta list-tables --json
|
|
160
|
+
maxc meta search-columns "id" --json
|
|
161
|
+
maxc meta describe your_table --json
|
|
162
|
+
maxc meta latest-partition your_table --json
|
|
163
|
+
maxc meta freshness your_table --json
|
|
164
|
+
maxc meta lineage your_table --json
|
|
165
|
+
maxc data sample your_table --partition ds=2026-03-20 --columns id,ds --rows 5 --json
|
|
166
|
+
maxc data profile your_table --partition ds=2026-03-20 --json
|
|
167
|
+
maxc query "SELECT 1 AS one" --json
|
|
168
|
+
maxc query cost "SELECT 1 AS one" --json
|
|
169
|
+
maxc query explain "SELECT 1 AS one" --json
|
|
170
|
+
maxc job submit "SELECT 1 AS one" --json
|
|
171
|
+
maxc job wait job_xxx --stream
|
|
172
|
+
maxc diff schema left_table right_table --json
|
|
173
|
+
maxc diff partition left_table right_table --json
|
|
174
|
+
maxc diff data left_table right_table --keys id --columns value_col --rows 100 --json
|
|
175
|
+
maxc cache status --json
|
|
176
|
+
maxc cache build-status --build-id build_xxx --json
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## `cache build --json` 行为
|
|
180
|
+
|
|
181
|
+
- `stdout` 只输出单个最终 JSON envelope
|
|
182
|
+
- `stderr` 持续输出进度文本,避免慢构建期间完全静默
|
|
183
|
+
- `--async --json` 会立即返回 `build_id`,随后用 `cache build-status --build-id <id> --json` 轮询
|
|
184
|
+
|
|
185
|
+
## JSON 契约
|
|
186
|
+
|
|
187
|
+
所有 `--json` 命令都返回 envelope:
|
|
188
|
+
|
|
189
|
+
- `version`
|
|
190
|
+
- `command`
|
|
191
|
+
- `command_id`
|
|
192
|
+
- `status`
|
|
193
|
+
- `data`
|
|
194
|
+
- `metadata`
|
|
195
|
+
- `error`
|
|
196
|
+
- `agent_hints`
|
|
197
|
+
|
|
198
|
+
常用规范化 `data` 结构:
|
|
199
|
+
|
|
200
|
+
- `auth whoami` -> `data.identity`
|
|
201
|
+
- `auth can-i` -> `data.authorization`
|
|
202
|
+
- `query` / `job wait` / `job result` -> `data.result` 和 `data.pagination`
|
|
203
|
+
- `query cost` / `query explain` -> `data.analysis`
|
|
204
|
+
- `meta describe` -> `data.table`
|
|
205
|
+
- `meta search` / `meta search-columns` -> `data.search.matches`
|
|
206
|
+
- `data sample` -> `data.sample`
|
|
207
|
+
- `data profile` -> `data.profile`
|
|
208
|
+
- `agent context` -> `data.context`
|
|
209
|
+
|
|
210
|
+
## 当前限制
|
|
211
|
+
|
|
212
|
+
- `meta lineage` 还没有接真实血缘 API;真实 backend 会明确返回 `supported=false`、`coverage=unsupported`
|
|
213
|
+
- `auth can-i` 当前只支持表级 `SELECT` 预检
|
|
214
|
+
- `auth login` 会把 AccessKey 明文写入本地 YAML;CLI 会尽量把文件权限收敛到 `0600`
|
|
215
|
+
- 环境变量优先于配置文件;`session_override.yaml` 对 project/schema 的优先级高于两者
|
|
216
|
+
- `meta list-tables` 是 cache-backed;冷启动时需要先执行 `cache build`
|
|
217
|
+
- `diff data` 当前是 keyed snapshot compare:每侧最多读取 `--rows` 行,不是全表 exhaustive diff
|
|
218
|
+
- 真实 backend 目前不提供统一 CU 口径成本,因此 `--cost-check` 在真实 backend 上不可用
|
|
219
|
+
- 真实 backend 的 `query explain / query cost` 当前基于 `execute_sql_cost` 和结构化 query outline,不是完整优化器执行计划树
|
|
220
|
+
- `--cursor` 当前是 CLI 侧 offset token,不是 MaxCompute 服务端游标
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
maxc_cli/__init__.py,sha256=gyVRundciHakyXcIdYpdgWapAbmk9MrRjJXvXgARn3o,74
|
|
2
|
+
maxc_cli/__main__.py,sha256=P_XzMrNHOza38_dmqmIuv9POOCRxf4RiBC4-VoR6xBw,81
|
|
3
|
+
maxc_cli/app.py,sha256=WDYmd9u8XCRaWXI7k68-8y-dwNpu8Uci0n7oV-rIk9E,134135
|
|
4
|
+
maxc_cli/audit.py,sha256=D_p2CMnxCZAQZy0_UkvTCH61DLvLItWJnnTvE9sDSFE,516
|
|
5
|
+
maxc_cli/auth_providers.py,sha256=gnf2vDtgcJXzab0CpJmbZDpAeJksaSuS4xGgRlMUhY8,17244
|
|
6
|
+
maxc_cli/cache.py,sha256=6xq2Qv9UcKDU8ogpqitNB37DnBVzw_3P-mBa24PGNMw,28560
|
|
7
|
+
maxc_cli/cli.py,sha256=Gyjp5uLyPCddAWDlRIzcJWgVmHMFTPzBuPle2CTnN3M,57414
|
|
8
|
+
maxc_cli/config.py,sha256=ahjAAgyGFoVoWDN4vlu2DVauYnhP0NgLA2gTJN-ef-k,14338
|
|
9
|
+
maxc_cli/exceptions.py,sha256=ZJr8NkEyiaB1WU3__VOxBtRkrNrKZh2Ymph6BS0RJXM,2177
|
|
10
|
+
maxc_cli/helpers.py,sha256=t4xrxhg6CpBirCf-EWJBWw-tYSGgssql7duCZt51zEY,35960
|
|
11
|
+
maxc_cli/models.py,sha256=lOR_KjKf3SDnbaDDtQVRVegcTlc8t_c3lGsbs_0QEIA,18517
|
|
12
|
+
maxc_cli/output.py,sha256=kvWIB6cXDQSrkiC3g_66L7OeHqf7H4KT8kT4k3mZPv8,2418
|
|
13
|
+
maxc_cli/store.py,sha256=b3QUkAHp_fV4m0ZCV2zx44KkNV3ZeOeVtx6V2Vy6VZw,4062
|
|
14
|
+
maxc_cli/utils.py,sha256=jwub0dt6V_EpV455MzGjoueNz6u4FXYU6SayaIOp9uk,4517
|
|
15
|
+
maxc_cli/backend/__init__.py,sha256=Tj1gfvBJlvneV6JO5I68bmnOSo3MC2Hq9hzur2hl9Gw,218
|
|
16
|
+
maxc_cli/backend/auth.py,sha256=V9PLN6r0MpT3gE1na67TD8UUnlB3fnoQYYpt5DAN_Rw,5662
|
|
17
|
+
maxc_cli/backend/data.py,sha256=yznGIucq1VJF3MKZB8IiZTIvCA8yAdAaFz9A3boRnWs,2940
|
|
18
|
+
maxc_cli/backend/job.py,sha256=OFdhU8drsY8n7j1TARuOwo51Ut78aT4JHQ02vlRTSQQ,11280
|
|
19
|
+
maxc_cli/backend/meta.py,sha256=INLtqpF9NUYyvHCBCYJknUN9b3br3HQ1Q0hTZI1oCr4,13702
|
|
20
|
+
maxc_cli/backend/odps.py,sha256=OquxZihCYK10HeswvwrePRyk34r50M-vq2EkoLf9-Kg,4864
|
|
21
|
+
maxc_cli/backend/query.py,sha256=QkyzxrLBtldL3is1WCB8ojkqHk-uuvBCVDjqSFPSOD0,5120
|
|
22
|
+
maxc_cli-0.1.0.dist-info/METADATA,sha256=xMONx9lCkuW8D3hEsBKw2lqoxHjOfzf3f-ZluW2ZVaA,6682
|
|
23
|
+
maxc_cli-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
24
|
+
maxc_cli-0.1.0.dist-info/entry_points.txt,sha256=T-OEXGdDzvWEW_TMh8PpPidJv6nsB3xCU4JbiRXnfWM,48
|
|
25
|
+
maxc_cli-0.1.0.dist-info/top_level.txt,sha256=2luw8czqtPh6ZOzQyGLxKHlTdao7sFY9RyhREBE6N8M,9
|
|
26
|
+
maxc_cli-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
maxc_cli
|