maxc-cli 0.1.0__tar.gz → 0.1.1__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.0 → maxc_cli-0.1.1}/PKG-INFO +1 -1
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/setup.py +1 -1
- maxc_cli-0.1.1/skills/use-maxc-cli/.DS_Store +0 -0
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/skills/use-maxc-cli/SKILL.md +76 -4
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/skills/use-maxc-cli/references/setup-install.md +7 -3
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/src/maxc_cli/__init__.py +1 -1
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/src/maxc_cli/app.py +36 -31
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/src/maxc_cli/backend/meta.py +15 -9
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/src/maxc_cli/cli.py +9 -3
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/src/maxc_cli.egg-info/PKG-INFO +1 -1
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/src/maxc_cli.egg-info/SOURCES.txt +1 -0
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/tests/test_agent_hints_and_cli.py +6 -10
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/tests/test_cli_mock.py +141 -0
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/MANIFEST.in +0 -0
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/README.md +0 -0
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/pyproject.toml +0 -0
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/scripts/sync_codex_skill.py +0 -0
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/setup.cfg +0 -0
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/skills/use-maxc-cli/agents/openai.yaml +0 -0
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/skills/use-maxc-cli/references/bootstrap-auth.md +0 -0
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/skills/use-maxc-cli/references/command-patterns.md +0 -0
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/src/maxc_cli/__main__.py +0 -0
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/src/maxc_cli/audit.py +0 -0
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/src/maxc_cli/auth_providers.py +0 -0
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/src/maxc_cli/backend/__init__.py +0 -0
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/src/maxc_cli/backend/auth.py +0 -0
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/src/maxc_cli/backend/data.py +0 -0
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/src/maxc_cli/backend/job.py +0 -0
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/src/maxc_cli/backend/odps.py +0 -0
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/src/maxc_cli/backend/query.py +0 -0
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/src/maxc_cli/cache.py +0 -0
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/src/maxc_cli/config.py +0 -0
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/src/maxc_cli/exceptions.py +0 -0
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/src/maxc_cli/helpers.py +0 -0
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/src/maxc_cli/models.py +0 -0
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/src/maxc_cli/output.py +0 -0
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/src/maxc_cli/store.py +0 -0
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/src/maxc_cli/utils.py +0 -0
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/src/maxc_cli.egg-info/dependency_links.txt +0 -0
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/src/maxc_cli.egg-info/entry_points.txt +0 -0
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/src/maxc_cli.egg-info/requires.txt +0 -0
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/src/maxc_cli.egg-info/top_level.txt +0 -0
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/tests/test_cache.py +0 -0
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/tests/test_compat.py +0 -0
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/tests/test_e2e_smoke.py +0 -0
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/tests/test_integration.py +0 -0
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/tests/test_integration_real.py +0 -0
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/tests/test_job_improvements.py +0 -0
- {maxc_cli-0.1.0 → maxc_cli-0.1.1}/tests/test_query_auto_promote.py +0 -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.1",
|
|
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",
|
|
Binary file
|
|
@@ -63,7 +63,7 @@ Then follow the corresponding section in [references/bootstrap-auth.md](referenc
|
|
|
63
63
|
- `configured=false` → no auth set up → **ask which method** (see Bootstrap Flow above).
|
|
64
64
|
- `configured=true, validation_status=failed` → config exists but remote check failed → inspect warnings, then fix or re-login.
|
|
65
65
|
3. Read [references/bootstrap-auth.md](references/bootstrap-auth.md) for auth paths.
|
|
66
|
-
4.
|
|
66
|
+
4. `meta list-tables --json` falls back to live queries on cache miss. Run `cache build --json` to speed up repeat queries.
|
|
67
67
|
5. Read [references/command-patterns.md](references/command-patterns.md) for command syntax and output shapes.
|
|
68
68
|
|
|
69
69
|
## Working Rules
|
|
@@ -75,7 +75,8 @@ Then follow the corresponding section in [references/bootstrap-auth.md](referenc
|
|
|
75
75
|
- Trust runtime help and actual command output over stale snippets.
|
|
76
76
|
- Never install or upgrade Python without explicit user confirmation.
|
|
77
77
|
- Prefer `auth login` over hand-editing `~/.maxc/config.yaml`.
|
|
78
|
-
- `meta list-tables` is cache-backed;
|
|
78
|
+
- `meta list-tables` is cache-backed; falls back to live backend query on cache miss.
|
|
79
|
+
- Most meta commands support `--schema` to override the session default (list-tables, search, search-columns).
|
|
79
80
|
- `session set/show/unset` are local-only — no authenticated backend required.
|
|
80
81
|
- `agent context` is a fast local config summary; does not enumerate tables.
|
|
81
82
|
- Use normalized `data` shapes: `auth whoami` → `data.identity`, `query`/`job result` → `data.result`, `meta describe` → `data.table`, `data sample` → `data.sample`.
|
|
@@ -87,7 +88,7 @@ Then follow the corresponding section in [references/bootstrap-auth.md](referenc
|
|
|
87
88
|
|---------|-----------------|
|
|
88
89
|
| Using `auth login --from-env` without checking env vars exist | Run `auth whoami --json` first; only use `--from-env` when env vars are confirmed set |
|
|
89
90
|
| Hand-editing `~/.maxc/config.yaml` | Use `auth login` |
|
|
90
|
-
| Calling `meta list-tables` on a cold cache |
|
|
91
|
+
| Calling `meta list-tables` on a cold cache | Tables are fetched live on cache miss; `cache build` improves speed for repeat queries |
|
|
91
92
|
| Inventing endpoints | Only use endpoints the user provided or that exist in current config |
|
|
92
93
|
| Using `job wait --stream` and expecting a JSON envelope | `--stream` emits NDJSON; use plain `job wait --json` for envelope |
|
|
93
94
|
| Running a query without checking cost first | Use `query cost` before large queries; use `--cost-check` to set auto-abort threshold |
|
|
@@ -146,6 +147,60 @@ maxc session unset --json
|
|
|
146
147
|
|
|
147
148
|
Session overrides are stored in `~/.maxc/session_override.yaml` and take priority over config files and env vars for project/schema only.
|
|
148
149
|
|
|
150
|
+
## Schema Operations
|
|
151
|
+
|
|
152
|
+
For projects with 3-tier namespace (project.schema.table):
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
# List available schemas
|
|
156
|
+
maxc meta list-schemas --json
|
|
157
|
+
|
|
158
|
+
# List tables in a specific schema (two approaches)
|
|
159
|
+
maxc meta list-tables --schema california_schools --json # one-shot
|
|
160
|
+
maxc session set --schema california_schools --json # sticky session
|
|
161
|
+
|
|
162
|
+
# Search within a schema
|
|
163
|
+
maxc meta search school --schema california_schools --json
|
|
164
|
+
maxc meta search-columns county --schema california_schools --json
|
|
165
|
+
|
|
166
|
+
# Build cache for a specific schema
|
|
167
|
+
maxc cache build --schema california_schools --json
|
|
168
|
+
|
|
169
|
+
# Describe a table (use schema.table_name format)
|
|
170
|
+
maxc meta describe california_schools.frpm --json
|
|
171
|
+
|
|
172
|
+
# Reset to default schema
|
|
173
|
+
maxc session unset --json
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
When `--schema` is given, it overrides `session set --schema`. When neither is set, the project default schema is used.
|
|
177
|
+
|
|
178
|
+
## Cache Mechanism
|
|
179
|
+
|
|
180
|
+
The metadata cache accelerates `list-tables`, `search`, `search-columns`, and `describe`.
|
|
181
|
+
|
|
182
|
+
- **How it works**: `cache build` fetches all table metadata from MaxCompute and stores it in a local SQLite DB (`~/.maxc/cache/cache.db`). Subsequent meta commands read from cache first.
|
|
183
|
+
- **Cache key**: `(project, schema_name, table_name)` — schema is part of the key, so different schemas have independent caches.
|
|
184
|
+
- **Cache miss behavior**: `list-tables` and `search` fall back to live backend queries on cache miss. No manual cache build is required, but caching speeds up repeated queries.
|
|
185
|
+
- **When to rebuild**: After schema changes, new tables, or when cache is stale. Check with `cache status --json`.
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
maxc cache build --json # build for current project/schema
|
|
189
|
+
maxc cache build --schema my_schema --json # build for specific schema
|
|
190
|
+
maxc cache status --json # check cache freshness
|
|
191
|
+
maxc cache clear --json # wipe and rebuild
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Known Limitations
|
|
195
|
+
|
|
196
|
+
| Feature | Status | Detail |
|
|
197
|
+
|---------|--------|--------|
|
|
198
|
+
| `meta lineage` | Placeholder | Returns `supported=false`; MaxCompute lineage API not yet integrated |
|
|
199
|
+
| `list-tables` pagination | Not implemented | CLI-side `--cursor` is offset token, not server-side cursor |
|
|
200
|
+
| `diff data` | Snapshot compare | Keyed snapshot compare, not exhaustive diff |
|
|
201
|
+
| `auth login` | Plaintext YAML | AccessKey stored in `~/.maxc/config.yaml` (file permissions 0600) |
|
|
202
|
+
| Write operations | Read-only | CLI enforces SELECT-only; DDL/DML not supported |
|
|
203
|
+
|
|
149
204
|
## Cost Control
|
|
150
205
|
|
|
151
206
|
Before running large queries, always estimate cost first:
|
|
@@ -210,9 +265,26 @@ maxc diff data prod_table staging_table \
|
|
|
210
265
|
--json
|
|
211
266
|
```
|
|
212
267
|
|
|
268
|
+
## Troubleshooting
|
|
269
|
+
|
|
270
|
+
| Symptom | Cause | Fix |
|
|
271
|
+
|---------|-------|-----|
|
|
272
|
+
| `list-tables` returns empty but tables exist | Wrong schema or no tables in default schema | Use `--schema <name>` or `session set --schema` |
|
|
273
|
+
| `search` returns no matches | Keyword not in table/column names or descriptions | Try broader keywords; check with `list-tables --schema` first |
|
|
274
|
+
| `cache build` reports 0 tables | Schema not specified for non-default schemas | Add `--schema <name>` |
|
|
275
|
+
| `describe` fails with NOT_FOUND | Table in a different schema | Use `schema.table_name` format or set session schema |
|
|
276
|
+
| Commands hang or timeout | Network/endpoint issue | Check `auth whoami --json` for endpoint; verify connectivity |
|
|
277
|
+
|
|
278
|
+
When all else fails, verify with raw pyodps:
|
|
279
|
+
```python
|
|
280
|
+
from odps import ODPS
|
|
281
|
+
o = ODPS(access_id, secret_key, project, endpoint)
|
|
282
|
+
list(o.list_tables(schema='<schema_name>'))
|
|
283
|
+
```
|
|
284
|
+
|
|
213
285
|
## Command Families
|
|
214
286
|
|
|
215
|
-
- Bootstrap: `python3 --version`, `pip install maxc-cli`, `python3 -m maxc_cli --help`
|
|
287
|
+
- Bootstrap: `python3 --version`, `pip install maxc-cli -i http://yum.tbsite.net/aliyun-pypi/simple --trusted-host yum.tbsite.net`, `python3 -m maxc_cli --help`
|
|
216
288
|
- Auth and session: `auth whoami`, `auth login`, `auth can-i`, `session set/show/unset`
|
|
217
289
|
- Metadata and data: `meta list-tables`, `meta describe`, `meta search`, `meta search-columns`, `meta latest-partition`, `meta freshness`, `meta partitions`, `meta list-projects`, `meta list-schemas`, `data sample`, `data profile`
|
|
218
290
|
- Query and jobs: `query`, `query cost`, `query explain`, `job submit/status/wait/result/diagnose/cancel/list`
|
|
@@ -44,16 +44,20 @@ Do not install or upgrade Python proactively. First tell the user why the curren
|
|
|
44
44
|
|
|
45
45
|
## Step 2: Install `maxc-cli`
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
`maxc-cli` 发布在内部 PyPI 仓库,安装时需要指定 index URL:
|
|
48
48
|
|
|
49
49
|
```bash
|
|
50
|
-
python3 -m pip install --upgrade maxc-cli
|
|
50
|
+
python3 -m pip install --upgrade maxc-cli \
|
|
51
|
+
-i http://yum.tbsite.net/aliyun-pypi/simple \
|
|
52
|
+
--trusted-host yum.tbsite.net
|
|
51
53
|
```
|
|
52
54
|
|
|
53
55
|
If the environment requires a user-local install:
|
|
54
56
|
|
|
55
57
|
```bash
|
|
56
|
-
python3 -m pip install --user --upgrade maxc-cli
|
|
58
|
+
python3 -m pip install --user --upgrade maxc-cli \
|
|
59
|
+
-i http://yum.tbsite.net/aliyun-pypi/simple \
|
|
60
|
+
--trusted-host yum.tbsite.net
|
|
57
61
|
```
|
|
58
62
|
|
|
59
63
|
Verify install with either the console script or module path:
|
|
@@ -820,12 +820,16 @@ class MaxCApp:
|
|
|
820
820
|
self.log("job.list", envelope.status, envelope.metadata)
|
|
821
821
|
return envelope
|
|
822
822
|
|
|
823
|
-
def meta_list_tables(self) -> 'Envelope':
|
|
823
|
+
def meta_list_tables(self, *, schema: 'str | None' = None) -> 'Envelope':
|
|
824
824
|
started = monotonic()
|
|
825
|
-
|
|
825
|
+
effective_schema = schema or self.config.default_schema
|
|
826
|
+
|
|
826
827
|
# Try to get from cache first
|
|
827
|
-
cached_tables = self.cache.get_all_cached_tables(
|
|
828
|
-
|
|
828
|
+
cached_tables = self.cache.get_all_cached_tables(
|
|
829
|
+
self.config.default_project,
|
|
830
|
+
schema_name=effective_schema,
|
|
831
|
+
)
|
|
832
|
+
|
|
829
833
|
if cached_tables:
|
|
830
834
|
# Use cached data (returns list of dicts)
|
|
831
835
|
tables = cached_tables
|
|
@@ -845,22 +849,20 @@ class MaxCApp:
|
|
|
845
849
|
for table in tables
|
|
846
850
|
]
|
|
847
851
|
else:
|
|
848
|
-
#
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
),
|
|
863
|
-
)
|
|
852
|
+
# Cache miss — fall back to live backend query
|
|
853
|
+
live_tables = self.backend.list_tables(schema=effective_schema)
|
|
854
|
+
source = "backend"
|
|
855
|
+
rows = [
|
|
856
|
+
{
|
|
857
|
+
"table_name": t.name,
|
|
858
|
+
"table_type": t.table_type or "TABLE",
|
|
859
|
+
"size_bytes": t.size_bytes,
|
|
860
|
+
"owner": t.owner,
|
|
861
|
+
"description": t.description,
|
|
862
|
+
"partition_columns": [c.name for c in (t.partition_columns or [])],
|
|
863
|
+
}
|
|
864
|
+
for t in live_tables
|
|
865
|
+
]
|
|
864
866
|
|
|
865
867
|
metadata = self._cache_metadata(
|
|
866
868
|
project=self.config.default_project,
|
|
@@ -981,14 +983,17 @@ class MaxCApp:
|
|
|
981
983
|
self.log("meta.describe", envelope.status, envelope.metadata)
|
|
982
984
|
return envelope
|
|
983
985
|
|
|
984
|
-
def meta_search(self, keyword: 'str') -> 'Envelope':
|
|
986
|
+
def meta_search(self, keyword: 'str', *, schema: 'str | None' = None) -> 'Envelope':
|
|
985
987
|
started = monotonic()
|
|
986
|
-
|
|
988
|
+
effective_schema = schema or self.config.default_schema
|
|
989
|
+
cached_tables = self.cache.get_all_cached_tables(
|
|
990
|
+
self.config.default_project, schema_name=effective_schema,
|
|
991
|
+
)
|
|
987
992
|
if cached_tables:
|
|
988
993
|
matches = self._search_in_cache(keyword, cached_tables)
|
|
989
994
|
source = "cache"
|
|
990
995
|
else:
|
|
991
|
-
matches = self.backend.search_tables(keyword)
|
|
996
|
+
matches = self.backend.search_tables(keyword, schema=effective_schema)
|
|
992
997
|
source = "live"
|
|
993
998
|
envelope = Envelope(
|
|
994
999
|
command="meta.search",
|
|
@@ -1007,14 +1012,17 @@ class MaxCApp:
|
|
|
1007
1012
|
self.log("meta.search", envelope.status, envelope.metadata)
|
|
1008
1013
|
return envelope
|
|
1009
1014
|
|
|
1010
|
-
def meta_search_columns(self, keyword: 'str') -> 'Envelope':
|
|
1015
|
+
def meta_search_columns(self, keyword: 'str', *, schema: 'str | None' = None) -> 'Envelope':
|
|
1011
1016
|
started = monotonic()
|
|
1012
|
-
|
|
1017
|
+
effective_schema = schema or self.config.default_schema
|
|
1018
|
+
cached_tables = self.cache.get_all_cached_tables(
|
|
1019
|
+
self.config.default_project, schema_name=effective_schema,
|
|
1020
|
+
)
|
|
1013
1021
|
if cached_tables:
|
|
1014
1022
|
matches = self._search_columns_in_cache(keyword, cached_tables)
|
|
1015
1023
|
source = "cache"
|
|
1016
1024
|
else:
|
|
1017
|
-
matches = self.backend.search_columns(keyword)
|
|
1025
|
+
matches = self.backend.search_columns(keyword, schema=effective_schema)
|
|
1018
1026
|
source = "live"
|
|
1019
1027
|
envelope = Envelope(
|
|
1020
1028
|
command="meta.search-columns",
|
|
@@ -1361,11 +1369,8 @@ class MaxCApp:
|
|
|
1361
1369
|
}
|
|
1362
1370
|
)
|
|
1363
1371
|
|
|
1364
|
-
all_tables = self.backend.list_tables()
|
|
1365
|
-
|
|
1366
|
-
tables = all_tables
|
|
1367
|
-
else:
|
|
1368
|
-
tables = all_tables
|
|
1372
|
+
all_tables = self.backend.list_tables(schema=schema_name)
|
|
1373
|
+
tables = all_tables
|
|
1369
1374
|
|
|
1370
1375
|
if progress_callback is not None:
|
|
1371
1376
|
progress_callback(
|
|
@@ -17,11 +17,14 @@ from ..helpers import (
|
|
|
17
17
|
class MetaMixin:
|
|
18
18
|
"""Mixin providing metadata methods."""
|
|
19
19
|
|
|
20
|
-
def list_tables(self) -> 'list[TableDefinition]':
|
|
21
|
-
"""List tables in the current project."""
|
|
20
|
+
def list_tables(self, *, schema: 'str | None' = None) -> 'list[TableDefinition]':
|
|
21
|
+
"""List tables in the current project, optionally filtered by schema."""
|
|
22
22
|
tables: 'list[TableDefinition]' = []
|
|
23
|
+
kwargs: 'dict[str, Any]' = {"project": self.project}
|
|
24
|
+
if schema:
|
|
25
|
+
kwargs["schema"] = schema
|
|
23
26
|
try:
|
|
24
|
-
for table in self.client.list_tables(
|
|
27
|
+
for table in self.client.list_tables(**kwargs):
|
|
25
28
|
tables.append(self._table_stub(table))
|
|
26
29
|
except Exception as exc:
|
|
27
30
|
raise translate_odps_error(exc) from exc
|
|
@@ -37,11 +40,11 @@ class MetaMixin:
|
|
|
37
40
|
definition.sample_rows = sample_rows
|
|
38
41
|
return definition
|
|
39
42
|
|
|
40
|
-
def search_tables(self, keyword: 'str') -> 'list[dict[str, Any]]':
|
|
43
|
+
def search_tables(self, keyword: 'str', *, schema: 'str | None' = None) -> 'list[dict[str, Any]]':
|
|
41
44
|
"""Search tables by keyword."""
|
|
42
45
|
tokens = [item.lower() for item in keyword.split() if item.strip()] or [keyword.lower()]
|
|
43
46
|
matches: 'list[dict[str, Any]]' = []
|
|
44
|
-
for table in self.list_tables():
|
|
47
|
+
for table in self.list_tables(schema=schema):
|
|
45
48
|
score = 0
|
|
46
49
|
searchable = f"{table.name} {table.description}".lower()
|
|
47
50
|
matched_columns: 'list[str]' = []
|
|
@@ -65,11 +68,11 @@ class MetaMixin:
|
|
|
65
68
|
)
|
|
66
69
|
return sorted(matches, key=lambda item: (-item["score"], item["table_name"]))
|
|
67
70
|
|
|
68
|
-
def search_columns(self, keyword: 'str') -> 'list[dict[str, Any]]':
|
|
71
|
+
def search_columns(self, keyword: 'str', *, schema: 'str | None' = None) -> 'list[dict[str, Any]]':
|
|
69
72
|
"""Search columns by keyword."""
|
|
70
73
|
tokens = [item.lower() for item in keyword.split() if item.strip()] or [keyword.lower()]
|
|
71
74
|
matches: 'list[dict[str, Any]]' = []
|
|
72
|
-
for table in self.list_tables():
|
|
75
|
+
for table in self.list_tables(schema=schema):
|
|
73
76
|
for column in table.columns:
|
|
74
77
|
score = 0
|
|
75
78
|
text = f"{column.name} {column.comment}".lower()
|
|
@@ -183,10 +186,13 @@ class MetaMixin:
|
|
|
183
186
|
|
|
184
187
|
# Private methods for metadata handling
|
|
185
188
|
|
|
186
|
-
def _get_table(self, table_name: 'str', *, project: 'str | None' = None):
|
|
189
|
+
def _get_table(self, table_name: 'str', *, project: 'str | None' = None, schema: 'str | None' = None):
|
|
187
190
|
"""Get ODPS table by name."""
|
|
191
|
+
kwargs: 'dict[str, Any]' = {"project": project or self.project}
|
|
192
|
+
if schema:
|
|
193
|
+
kwargs["schema"] = schema
|
|
188
194
|
try:
|
|
189
|
-
return self.client.get_table(table_name,
|
|
195
|
+
return self.client.get_table(table_name, **kwargs)
|
|
190
196
|
except Exception as exc:
|
|
191
197
|
raise translate_odps_error(exc) from exc
|
|
192
198
|
|
|
@@ -121,6 +121,7 @@ def build_parser() -> 'argparse.ArgumentParser':
|
|
|
121
121
|
meta_subparsers = _add_required_subparsers(meta_parser, dest="meta_command")
|
|
122
122
|
|
|
123
123
|
meta_list = meta_subparsers.add_parser("list-tables", help="List tables")
|
|
124
|
+
meta_list.add_argument("--schema", help="Schema name (overrides session default)")
|
|
124
125
|
meta_list.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
125
126
|
meta_list.set_defaults(handler=_handle_meta_list_tables)
|
|
126
127
|
|
|
@@ -132,11 +133,13 @@ def build_parser() -> 'argparse.ArgumentParser':
|
|
|
132
133
|
|
|
133
134
|
meta_search = meta_subparsers.add_parser("search", help="Search tables")
|
|
134
135
|
meta_search.add_argument("keyword", help="Search keyword")
|
|
136
|
+
meta_search.add_argument("--schema", help="Schema name (overrides session default)")
|
|
135
137
|
meta_search.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
136
138
|
meta_search.set_defaults(handler=_handle_meta_search)
|
|
137
139
|
|
|
138
140
|
meta_search_columns = meta_subparsers.add_parser("search-columns", help="Search columns")
|
|
139
141
|
meta_search_columns.add_argument("keyword", help="Search keyword")
|
|
142
|
+
meta_search_columns.add_argument("--schema", help="Schema name (overrides session default)")
|
|
140
143
|
meta_search_columns.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
141
144
|
meta_search_columns.set_defaults(handler=_handle_meta_search_columns)
|
|
142
145
|
|
|
@@ -542,7 +545,8 @@ def _handle_job_list(app: 'MaxCApp', args: 'argparse.Namespace', stdout: 'TextIO
|
|
|
542
545
|
|
|
543
546
|
|
|
544
547
|
def _handle_meta_list_tables(app: 'MaxCApp', args: 'argparse.Namespace', stdout: 'TextIO') -> 'None':
|
|
545
|
-
|
|
548
|
+
schema = getattr(args, "schema", None)
|
|
549
|
+
envelope = app.meta_list_tables(schema=schema)
|
|
546
550
|
_emit_envelope(envelope, args=args, stdout=stdout, default_format="table")
|
|
547
551
|
|
|
548
552
|
|
|
@@ -552,12 +556,14 @@ def _handle_meta_describe(app: 'MaxCApp', args: 'argparse.Namespace', stdout: 'T
|
|
|
552
556
|
|
|
553
557
|
|
|
554
558
|
def _handle_meta_search(app: 'MaxCApp', args: 'argparse.Namespace', stdout: 'TextIO') -> 'None':
|
|
555
|
-
|
|
559
|
+
schema = getattr(args, "schema", None)
|
|
560
|
+
envelope = app.meta_search(args.keyword, schema=schema)
|
|
556
561
|
_emit_envelope(envelope, args=args, stdout=stdout, default_format="table")
|
|
557
562
|
|
|
558
563
|
|
|
559
564
|
def _handle_meta_search_columns(app: 'MaxCApp', args: 'argparse.Namespace', stdout: 'TextIO') -> 'None':
|
|
560
|
-
|
|
565
|
+
schema = getattr(args, "schema", None)
|
|
566
|
+
envelope = app.meta_search_columns(args.keyword, schema=schema)
|
|
561
567
|
_emit_envelope(envelope, args=args, stdout=stdout, default_format="table")
|
|
562
568
|
|
|
563
569
|
|
|
@@ -169,7 +169,7 @@ def _table(name: 'str' = "sales.orders") -> 'TableDefinition':
|
|
|
169
169
|
|
|
170
170
|
|
|
171
171
|
class _StubMetaBackend:
|
|
172
|
-
def list_tables(self) -> 'list[TableDefinition]':
|
|
172
|
+
def list_tables(self, *, schema: 'str | None' = None) -> 'list[TableDefinition]':
|
|
173
173
|
return [_table()]
|
|
174
174
|
|
|
175
175
|
def describe_table(self, table_name: 'str') -> 'TableDefinition':
|
|
@@ -190,19 +190,15 @@ def _make_app(tmp_path: 'Path') -> 'MaxCApp':
|
|
|
190
190
|
return app
|
|
191
191
|
|
|
192
192
|
|
|
193
|
-
def
|
|
193
|
+
def test_meta_list_tables_returns_live_results_when_cache_is_empty(tmp_path: 'Path') -> 'None':
|
|
194
194
|
app = _make_app(tmp_path)
|
|
195
195
|
|
|
196
196
|
envelope = app.meta_list_tables()
|
|
197
197
|
|
|
198
|
-
assert envelope.status == "
|
|
199
|
-
|
|
200
|
-
assert
|
|
201
|
-
assert
|
|
202
|
-
"tables": [],
|
|
203
|
-
"pagination": {"total": 0, "has_more": False},
|
|
204
|
-
}
|
|
205
|
-
assert envelope.to_dict()["agent_hints"]["action_ids"] == ["cache.build"]
|
|
198
|
+
assert envelope.status == "success"
|
|
199
|
+
tables = envelope.to_dict()["data"]["tables"]
|
|
200
|
+
assert len(tables) == 1
|
|
201
|
+
assert tables[0]["table_name"] == "sales.orders"
|
|
206
202
|
|
|
207
203
|
|
|
208
204
|
def test_cache_build_returns_clear_metadata_and_async_build_completes(tmp_path: 'Path') -> 'None':
|
|
@@ -1207,3 +1207,144 @@ def test_not_found_error_renders_markdown_without_json_flag(
|
|
|
1207
1207
|
assert "**Error**" in err_text
|
|
1208
1208
|
assert "`NOT_FOUND`" in err_text
|
|
1209
1209
|
assert "**Suggestion**" in err_text
|
|
1210
|
+
|
|
1211
|
+
|
|
1212
|
+
# ============================================================
|
|
1213
|
+
# Schema Passthrough Tests
|
|
1214
|
+
# ============================================================
|
|
1215
|
+
|
|
1216
|
+
|
|
1217
|
+
class _SchemaAwareODPS(FakeODPS):
|
|
1218
|
+
"""Mock ODPS client that returns different tables per schema."""
|
|
1219
|
+
|
|
1220
|
+
_SCHEMA_TABLES = {
|
|
1221
|
+
None: ["default_table_a", "default_table_b"],
|
|
1222
|
+
"california_schools": ["frpm", "satscores", "schools"],
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
def list_tables(self, *, project=None, schema=None):
|
|
1226
|
+
names = self._SCHEMA_TABLES.get(schema, [])
|
|
1227
|
+
return [
|
|
1228
|
+
type("FakeTable", (), {"name": n})()
|
|
1229
|
+
for n in names
|
|
1230
|
+
]
|
|
1231
|
+
|
|
1232
|
+
def get_table(self, name, *, project=None, schema=None):
|
|
1233
|
+
# minimal stub for describe
|
|
1234
|
+
return type("FakeTable", (), {
|
|
1235
|
+
"name": name,
|
|
1236
|
+
"comment": "",
|
|
1237
|
+
"table_schema": type("Schema", (), {"columns": [], "partitions": []})(),
|
|
1238
|
+
"owner": "test_owner",
|
|
1239
|
+
"creation_time": None,
|
|
1240
|
+
"last_data_modified_time": None,
|
|
1241
|
+
"is_virtual_view": False,
|
|
1242
|
+
"size": 0,
|
|
1243
|
+
"lifecycle": None,
|
|
1244
|
+
})()
|
|
1245
|
+
|
|
1246
|
+
|
|
1247
|
+
def test_meta_list_tables_passes_schema_to_backend(
|
|
1248
|
+
tmp_path: 'Path', monkeypatch
|
|
1249
|
+
) -> None:
|
|
1250
|
+
"""meta list-tables --schema should list tables from the specified schema."""
|
|
1251
|
+
clear_odps_env(monkeypatch)
|
|
1252
|
+
isolate_home(monkeypatch, tmp_path)
|
|
1253
|
+
import odps
|
|
1254
|
+
monkeypatch.setattr(odps, "ODPS", _SchemaAwareODPS)
|
|
1255
|
+
|
|
1256
|
+
config_path = _make_config_with_odps(tmp_path)
|
|
1257
|
+
code, payload, _ = run_json_command(
|
|
1258
|
+
tmp_path, config_path,
|
|
1259
|
+
["meta", "list-tables", "--schema", "california_schools", "--json"],
|
|
1260
|
+
)
|
|
1261
|
+
|
|
1262
|
+
assert code == 0
|
|
1263
|
+
assert payload["status"] == "success"
|
|
1264
|
+
table_names = [t["table_name"] for t in payload["data"]["tables"]]
|
|
1265
|
+
assert sorted(table_names) == ["frpm", "satscores", "schools"]
|
|
1266
|
+
|
|
1267
|
+
|
|
1268
|
+
def test_meta_list_tables_without_schema_uses_default(
|
|
1269
|
+
tmp_path: 'Path', monkeypatch
|
|
1270
|
+
) -> None:
|
|
1271
|
+
"""meta list-tables without --schema should list tables from default schema."""
|
|
1272
|
+
clear_odps_env(monkeypatch)
|
|
1273
|
+
isolate_home(monkeypatch, tmp_path)
|
|
1274
|
+
import odps
|
|
1275
|
+
monkeypatch.setattr(odps, "ODPS", _SchemaAwareODPS)
|
|
1276
|
+
|
|
1277
|
+
config_path = _make_config_with_odps(tmp_path)
|
|
1278
|
+
code, payload, _ = run_json_command(
|
|
1279
|
+
tmp_path, config_path,
|
|
1280
|
+
["meta", "list-tables", "--json"],
|
|
1281
|
+
)
|
|
1282
|
+
|
|
1283
|
+
assert code == 0
|
|
1284
|
+
assert payload["status"] == "success"
|
|
1285
|
+
table_names = [t["table_name"] for t in payload["data"]["tables"]]
|
|
1286
|
+
assert sorted(table_names) == ["default_table_a", "default_table_b"]
|
|
1287
|
+
|
|
1288
|
+
|
|
1289
|
+
def test_cache_build_passes_schema_to_backend(
|
|
1290
|
+
tmp_path: 'Path', monkeypatch
|
|
1291
|
+
) -> None:
|
|
1292
|
+
"""cache build --schema should list tables from the specified schema."""
|
|
1293
|
+
clear_odps_env(monkeypatch)
|
|
1294
|
+
isolate_home(monkeypatch, tmp_path)
|
|
1295
|
+
import odps
|
|
1296
|
+
monkeypatch.setattr(odps, "ODPS", _SchemaAwareODPS)
|
|
1297
|
+
|
|
1298
|
+
config_path = _make_config_with_odps(tmp_path)
|
|
1299
|
+
code, payload, _ = run_json_command(
|
|
1300
|
+
tmp_path, config_path,
|
|
1301
|
+
["cache", "build", "--schema", "california_schools", "--json"],
|
|
1302
|
+
)
|
|
1303
|
+
|
|
1304
|
+
assert code == 0
|
|
1305
|
+
assert payload["data"]["tables_scanned"] == 3
|
|
1306
|
+
assert payload["data"]["cached_tables"] == 3
|
|
1307
|
+
|
|
1308
|
+
|
|
1309
|
+
def test_meta_search_passes_schema_to_backend(
|
|
1310
|
+
tmp_path: 'Path', monkeypatch
|
|
1311
|
+
) -> None:
|
|
1312
|
+
"""meta search --schema should search tables in the specified schema."""
|
|
1313
|
+
clear_odps_env(monkeypatch)
|
|
1314
|
+
isolate_home(monkeypatch, tmp_path)
|
|
1315
|
+
import odps
|
|
1316
|
+
monkeypatch.setattr(odps, "ODPS", _SchemaAwareODPS)
|
|
1317
|
+
|
|
1318
|
+
config_path = _make_config_with_odps(tmp_path)
|
|
1319
|
+
code, payload, _ = run_json_command(
|
|
1320
|
+
tmp_path, config_path,
|
|
1321
|
+
["meta", "search", "frpm", "--schema", "california_schools", "--json"],
|
|
1322
|
+
)
|
|
1323
|
+
|
|
1324
|
+
assert code == 0
|
|
1325
|
+
assert payload["status"] == "success"
|
|
1326
|
+
matches = payload["data"]["search"]["matches"]
|
|
1327
|
+
assert len(matches) >= 1
|
|
1328
|
+
assert any(m["table_name"] == "frpm" for m in matches)
|
|
1329
|
+
|
|
1330
|
+
|
|
1331
|
+
def test_meta_search_columns_passes_schema_to_backend(
|
|
1332
|
+
tmp_path: 'Path', monkeypatch
|
|
1333
|
+
) -> None:
|
|
1334
|
+
"""meta search-columns --schema should search in the specified schema."""
|
|
1335
|
+
clear_odps_env(monkeypatch)
|
|
1336
|
+
isolate_home(monkeypatch, tmp_path)
|
|
1337
|
+
import odps
|
|
1338
|
+
monkeypatch.setattr(odps, "ODPS", _SchemaAwareODPS)
|
|
1339
|
+
|
|
1340
|
+
config_path = _make_config_with_odps(tmp_path)
|
|
1341
|
+
# search for a keyword that won't match (stub tables have no columns),
|
|
1342
|
+
# but verify it runs without error and uses the right schema
|
|
1343
|
+
code, payload, _ = run_json_command(
|
|
1344
|
+
tmp_path, config_path,
|
|
1345
|
+
["meta", "search-columns", "nonexistent", "--schema", "california_schools", "--json"],
|
|
1346
|
+
)
|
|
1347
|
+
|
|
1348
|
+
assert code == 0
|
|
1349
|
+
assert payload["status"] == "success"
|
|
1350
|
+
assert payload["data"]["search"]["matches"] == []
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|