maxc-cli 0.9.3__tar.gz → 0.9.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.9.3 → maxc_cli-0.9.5}/PKG-INFO +25 -2
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/README.md +24 -1
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/__init__.py +1 -1
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/app.py +20 -2
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/backend/auth.py +80 -20
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/backend/catalog.py +6 -2
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/backend/odps.py +40 -3
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/backend/semantic.py +48 -15
- maxc_cli-0.9.5/src/maxc_cli/catalog_endpoint.py +42 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/cli.py +4 -2
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/config.py +1 -1
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/semantic_management.py +1 -1
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/skills/references/semantic-packages.md +5 -2
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli.egg-info/PKG-INFO +25 -2
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli.egg-info/SOURCES.txt +2 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_ai_native_contract_regressions.py +19 -2
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_query_result_csv_fallback.py +110 -0
- maxc_cli-0.9.5/tests/test_semantic_identity_endpoints.py +93 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_semantic_management.py +14 -2
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/MANIFEST.in +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/pyproject.toml +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/scripts/pyinstaller_entry.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/scripts/regression_test.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/setup.cfg +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/setup.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/__main__.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/_samples.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/agent_platforms.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/audit.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/auth_continuation.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/auth_providers.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/backend/__init__.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/backend/data.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/backend/job.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/backend/mcp.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/backend/meta.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/backend/query.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/cache.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/catalog_bootstrap.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/enterprise_tls.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/exceptions.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/function_metadata.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/help_format.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/helpers.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/job_ids.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/masking.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/mcp_serve.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/models.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/oauth.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/odps_runtime.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/output.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/proxy_auth.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/semantic.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/setting_parser.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/skills/SKILL.md +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/skills/agents/openai.yaml +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/skills/references/bootstrap-auth.md +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/skills/references/bootstrap-flow.md +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/skills/references/command-patterns.md +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/skills/references/json-output-format.md +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/skills/references/maxcompute-select-guide.md +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/skills/references/maxcompute-sql-notes.md +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/skills/references/partition-guide.md +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/skills/references/red-lines.md +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/skills/references/registered-functions.md +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/skills/references/setup-install.md +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/skills/references/sql-common-errors.md +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/skills/references/sql-query-patterns.md +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/skills/references/text2sql-principles.md +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/state_permissions.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/store.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli/utils.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli.egg-info/dependency_links.txt +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli.egg-info/entry_points.txt +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli.egg-info/requires.txt +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/src/maxc_cli.egg-info/top_level.txt +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_agent_hints_and_cli.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_agent_platforms.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_agent_skill_commands.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_agent_skill_commands_context.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_auth_logout.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_backend_auth.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_backend_data.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_backend_data_serialization.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_backend_mcp.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_backend_meta.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_backend_query_cost.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_build_release_archive_compat.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_build_release_script.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_cache.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_catalog.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_catalog_bootstrap.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_cli_arg_validation.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_cli_mock.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_cli_query_parse_and_sanitize.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_compat.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_config_atomic_write.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_connection_bootstrap.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_e2e_smoke.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_effective_hints_contract.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_enterprise_tls.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_envelope_shape.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_error_self_correction.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_error_translation.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_exit_codes.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_external_auth.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_flag_hoist.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_help_format.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_help_version_e2e.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_helpers.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_helpers_csv.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_installer_contracts.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_integration.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_integration_real.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_job_improvements.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_job_inspection.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_job_store_durability.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_kb_commands.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_manifest_runtime_contract.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_masking.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_mcp_serve.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_meta_schema_and_partition_cols.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_oauth.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_odps_runtime.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_output_action_safety.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_output_format_contract.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_packaging_metadata.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_phase1_improvements.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_proxy_auth.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_pyinstaller_bundle.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_python39_compat.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_query_auto_promote.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_query_namespace_settings.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_registered_functions.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_semantic_scope.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_semantic_transport.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_setting_parser.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_skill_cli_consistency.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_skill_eval.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_skill_renderer.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_startup_imports.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_state_permissions.py +0 -0
- {maxc_cli-0.9.3 → maxc_cli-0.9.5}/tests/test_state_portability.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: maxc-cli
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.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.9
|
|
@@ -195,6 +195,18 @@ aliyun maxc agent skill --json # Skill 路径、
|
|
|
195
195
|
maxc meta describe my_table --json
|
|
196
196
|
```
|
|
197
197
|
|
|
198
|
+
### 客户端结果脱敏
|
|
199
|
+
|
|
200
|
+
从 0.9.5 起,CLI 默认保留服务端返回的值。若要让 CLI 按敏感列名(例如
|
|
201
|
+
`email`、`phone`)额外遮盖结果,可在 `~/.maxc/config.yaml` 中设置:
|
|
202
|
+
|
|
203
|
+
```yaml
|
|
204
|
+
masking:
|
|
205
|
+
enabled: true
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
MaxCompute 服务端脱敏仍按项目策略执行。
|
|
209
|
+
|
|
198
210
|
### safety 块
|
|
199
211
|
|
|
200
212
|
`query` 和 `job` 命令的 `data` 中包含 `safety` 字段,描述安全策略决策:
|
|
@@ -304,7 +316,7 @@ To return to local signing, explicitly run the appropriate `auth login` command.
|
|
|
304
316
|
## Versioned semantic packages
|
|
305
317
|
|
|
306
318
|
`maxc semantic` manages remote Catalog packages using the current connection.
|
|
307
|
-
|
|
319
|
+
The namespace is auto-discovered from verified authenticated account identity when possible; otherwise pass an independently verified main-account ID with `--namespace`. Available operations:
|
|
308
320
|
`list`, `get`, `revisions`, `export`, `create`, `diff`, `apply`, `publish`, `delete`.
|
|
309
321
|
`meta semantic` continues to manage local annotations.
|
|
310
322
|
|
|
@@ -324,6 +336,7 @@ write must be reconciled before retrying. Publication checks are not SQL or
|
|
|
324
336
|
business validation. History lists the latest 20 summaries, and an immutable
|
|
325
337
|
revision read does not guarantee historical DataBridge analysis.
|
|
326
338
|
|
|
339
|
+
|
|
327
340
|
### Task metrics and worker logs
|
|
328
341
|
|
|
329
342
|
```bash
|
|
@@ -338,3 +351,13 @@ Task detail preserves the service payload (including `mapReduce.jsonSummary`).
|
|
|
338
351
|
Select `log_id` from `data.workers`; logs are returned as `data.content` in the JSON
|
|
339
352
|
envelope. The requested size defaults to 1 MiB and must be positive; returned logs
|
|
340
353
|
may be partial. Use `--project` to select the owning project explicitly.
|
|
354
|
+
|
|
355
|
+
Source-specific dbt semantic conversion is maintained by dbt-maxcompute (`dbt run-operation dbt_maxcompute.export_semantic_package`). maxc supplies generic package management and `meta describe --metadata-only` for live schema inspection.
|
|
356
|
+
|
|
357
|
+
## 语义包试用版
|
|
358
|
+
|
|
359
|
+
```bash
|
|
360
|
+
python -m pip install 'maxc-cli==0.9.5b1'
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
0.9.5b1 支持从可信账号身份解析 namespace,兼容旧版 PyODPS 返回的 Catalog 域名,并提供 `meta describe --metadata-only`。参见[语义包中文指南](docs/semantic-packages.zh-CN.md)。dbt 转换使用适配器自带的 `dbt run-operation dbt_maxcompute.export_semantic_package` 宏。
|
|
@@ -173,6 +173,18 @@ aliyun maxc agent skill --json # Skill 路径、
|
|
|
173
173
|
maxc meta describe my_table --json
|
|
174
174
|
```
|
|
175
175
|
|
|
176
|
+
### 客户端结果脱敏
|
|
177
|
+
|
|
178
|
+
从 0.9.5 起,CLI 默认保留服务端返回的值。若要让 CLI 按敏感列名(例如
|
|
179
|
+
`email`、`phone`)额外遮盖结果,可在 `~/.maxc/config.yaml` 中设置:
|
|
180
|
+
|
|
181
|
+
```yaml
|
|
182
|
+
masking:
|
|
183
|
+
enabled: true
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
MaxCompute 服务端脱敏仍按项目策略执行。
|
|
187
|
+
|
|
176
188
|
### safety 块
|
|
177
189
|
|
|
178
190
|
`query` 和 `job` 命令的 `data` 中包含 `safety` 字段,描述安全策略决策:
|
|
@@ -282,7 +294,7 @@ To return to local signing, explicitly run the appropriate `auth login` command.
|
|
|
282
294
|
## Versioned semantic packages
|
|
283
295
|
|
|
284
296
|
`maxc semantic` manages remote Catalog packages using the current connection.
|
|
285
|
-
|
|
297
|
+
The namespace is auto-discovered from verified authenticated account identity when possible; otherwise pass an independently verified main-account ID with `--namespace`. Available operations:
|
|
286
298
|
`list`, `get`, `revisions`, `export`, `create`, `diff`, `apply`, `publish`, `delete`.
|
|
287
299
|
`meta semantic` continues to manage local annotations.
|
|
288
300
|
|
|
@@ -302,6 +314,7 @@ write must be reconciled before retrying. Publication checks are not SQL or
|
|
|
302
314
|
business validation. History lists the latest 20 summaries, and an immutable
|
|
303
315
|
revision read does not guarantee historical DataBridge analysis.
|
|
304
316
|
|
|
317
|
+
|
|
305
318
|
### Task metrics and worker logs
|
|
306
319
|
|
|
307
320
|
```bash
|
|
@@ -316,3 +329,13 @@ Task detail preserves the service payload (including `mapReduce.jsonSummary`).
|
|
|
316
329
|
Select `log_id` from `data.workers`; logs are returned as `data.content` in the JSON
|
|
317
330
|
envelope. The requested size defaults to 1 MiB and must be positive; returned logs
|
|
318
331
|
may be partial. Use `--project` to select the owning project explicitly.
|
|
332
|
+
|
|
333
|
+
Source-specific dbt semantic conversion is maintained by dbt-maxcompute (`dbt run-operation dbt_maxcompute.export_semantic_package`). maxc supplies generic package management and `meta describe --metadata-only` for live schema inspection.
|
|
334
|
+
|
|
335
|
+
## 语义包试用版
|
|
336
|
+
|
|
337
|
+
```bash
|
|
338
|
+
python -m pip install 'maxc-cli==0.9.5b1'
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
0.9.5b1 支持从可信账号身份解析 namespace,兼容旧版 PyODPS 返回的 Catalog 域名,并提供 `meta describe --metadata-only`。参见[语义包中文指南](docs/semantic-packages.zh-CN.md)。dbt 转换使用适配器自带的 `dbt run-operation dbt_maxcompute.export_semantic_package` 宏。
|
|
@@ -2412,6 +2412,7 @@ class MaxCApp:
|
|
|
2412
2412
|
project: 'str | None' = None,
|
|
2413
2413
|
*,
|
|
2414
2414
|
schema: 'str | None' = None,
|
|
2415
|
+
metadata_only: bool = False,
|
|
2415
2416
|
) -> 'Envelope':
|
|
2416
2417
|
started = monotonic()
|
|
2417
2418
|
(
|
|
@@ -2422,6 +2423,23 @@ class MaxCApp:
|
|
|
2422
2423
|
qualified_name,
|
|
2423
2424
|
) = self._resolve_table_scope(table_name, project=project, schema=schema)
|
|
2424
2425
|
|
|
2426
|
+
if metadata_only:
|
|
2427
|
+
table = self.backend.describe_table_metadata(
|
|
2428
|
+
target_table, project=target_project, schema=effective_schema,
|
|
2429
|
+
)
|
|
2430
|
+
payload = {
|
|
2431
|
+
"table_name": table.name,
|
|
2432
|
+
"schema_name": effective_schema,
|
|
2433
|
+
"project": target_project,
|
|
2434
|
+
"columns": [{"name": c.name, "type": c.type} for c in table.columns],
|
|
2435
|
+
"partition_columns": [{"name": c.name, "type": c.type} for c in table.partition_columns or []],
|
|
2436
|
+
"has_more_columns": False,
|
|
2437
|
+
}
|
|
2438
|
+
envelope = Envelope(command="meta.describe", status="success", data=payload,
|
|
2439
|
+
metadata={"source": "live", "metadata_only": True})
|
|
2440
|
+
self.log("meta.describe", envelope.status, envelope.metadata)
|
|
2441
|
+
return envelope
|
|
2442
|
+
|
|
2425
2443
|
# Try to get from cache first
|
|
2426
2444
|
cached_table = self.cache.get_cached_table(
|
|
2427
2445
|
target_project,
|
|
@@ -2931,14 +2949,14 @@ class MaxCApp:
|
|
|
2931
2949
|
raise ValidationError("--schema must not be empty.")
|
|
2932
2950
|
return target_project, target_schema
|
|
2933
2951
|
|
|
2934
|
-
def semantic_manage(self, operation: str, *, namespace: str, **options) -> 'Envelope':
|
|
2952
|
+
def semantic_manage(self, operation: str, *, namespace: 'str | None' = None, **options) -> 'Envelope':
|
|
2935
2953
|
"""Run a remote SemanticSpec workflow, separate from local annotations."""
|
|
2936
2954
|
from .semantic_management import SemanticManager
|
|
2937
2955
|
|
|
2938
2956
|
manager = SemanticManager(self.backend, namespace)
|
|
2939
2957
|
data = manager.execute(operation, **options)
|
|
2940
2958
|
command = "semantic." + operation
|
|
2941
|
-
metadata = {"namespace": namespace}
|
|
2959
|
+
metadata = {"namespace": manager.namespace}
|
|
2942
2960
|
envelope = Envelope(command=command, status="success", data=data,
|
|
2943
2961
|
metadata=metadata, agent_hints=AgentHints(warnings=manager.warnings))
|
|
2944
2962
|
self.log(command, envelope.status, metadata)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Auth-related mixin for OdpsBackend."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import re
|
|
4
5
|
from typing import Any
|
|
5
6
|
from urllib.parse import quote
|
|
6
7
|
from xml.etree import ElementTree
|
|
@@ -14,10 +15,52 @@ from ..helpers import (
|
|
|
14
15
|
)
|
|
15
16
|
|
|
16
17
|
|
|
18
|
+
def semantic_account_identity(result):
|
|
19
|
+
"""Resolve only server-verified IDs; never infer ownership from a project."""
|
|
20
|
+
raw = result if isinstance(result, dict) else {}
|
|
21
|
+
|
|
22
|
+
def numeric(value):
|
|
23
|
+
return value if isinstance(value, str) and re.fullmatch(r"[0-9]{1,32}", value) else None
|
|
24
|
+
|
|
25
|
+
raw_id = raw.get("ID")
|
|
26
|
+
display = raw.get("DisplayName")
|
|
27
|
+
display = display if isinstance(display, str) else ""
|
|
28
|
+
is_main = bool(re.fullmatch(r"ALIYUN\$[^$:\s]+", display))
|
|
29
|
+
is_ram = display.startswith("RAM$")
|
|
30
|
+
composite = (
|
|
31
|
+
re.fullmatch(r"([0-9]{1,32}):([0-9]{1,32}|role/[A-Za-z0-9_+=,.@-]{1,128})", raw_id)
|
|
32
|
+
if isinstance(raw_id, str)
|
|
33
|
+
else None
|
|
34
|
+
)
|
|
35
|
+
principal = raw_id if composite else numeric(raw_id)
|
|
36
|
+
kind = (
|
|
37
|
+
"MAIN_ACCOUNT"
|
|
38
|
+
if is_main
|
|
39
|
+
else ("RAM_ROLE" if is_ram and ":role/" in display else "RAM_USER" if is_ram else "UNKNOWN")
|
|
40
|
+
)
|
|
41
|
+
account = numeric(raw.get("AccountId"))
|
|
42
|
+
source = "whoami.AccountId" if account else None
|
|
43
|
+
resolved = numeric(raw_id) if is_main else composite.group(1) if is_ram and composite else None
|
|
44
|
+
if resolved:
|
|
45
|
+
if account and account != resolved:
|
|
46
|
+
raise ValidationError(
|
|
47
|
+
"Conflicting account identity returned by whoami; namespace discovery stopped."
|
|
48
|
+
)
|
|
49
|
+
account = resolved
|
|
50
|
+
source = "whoami.main_account.ID" if is_main else "whoami.RAM.ID.account_prefix"
|
|
51
|
+
return {
|
|
52
|
+
"principal_id": principal,
|
|
53
|
+
"principal_type": kind,
|
|
54
|
+
"account_id": account,
|
|
55
|
+
"semantic_namespace": account,
|
|
56
|
+
"semantic_namespace_source": source,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
17
60
|
class AuthMixin:
|
|
18
61
|
"""Mixin providing authentication and authorization methods."""
|
|
19
62
|
|
|
20
|
-
def whoami_info(self, *, project:
|
|
63
|
+
def whoami_info(self, *, project: "str | None" = None) -> "tuple[dict[str, Any], list[str]]":
|
|
21
64
|
"""Get current identity info by executing ``whoami`` security query.
|
|
22
65
|
|
|
23
66
|
Calls ``client.execute_security_query("whoami")`` to verify the
|
|
@@ -38,7 +81,7 @@ class AuthMixin:
|
|
|
38
81
|
owner_display_name = result.get("DisplayName") if isinstance(result, dict) else None
|
|
39
82
|
if owner_display_name:
|
|
40
83
|
self._owner_display_name = owner_display_name
|
|
41
|
-
|
|
84
|
+
payload, warnings = build_odps_identity_payload(
|
|
42
85
|
client=self.client,
|
|
43
86
|
settings=self.settings,
|
|
44
87
|
allowed_operations=self.config.allowed_operations,
|
|
@@ -48,16 +91,33 @@ class AuthMixin:
|
|
|
48
91
|
project=target_project,
|
|
49
92
|
owner_display_name=owner_display_name,
|
|
50
93
|
)
|
|
94
|
+
identity = semantic_account_identity(result)
|
|
95
|
+
payload.update(identity)
|
|
96
|
+
if identity["semantic_namespace"] is None:
|
|
97
|
+
warnings.append(
|
|
98
|
+
"Main-account namespace could not be verified; pass an independently verified --namespace for semantic commands."
|
|
99
|
+
)
|
|
100
|
+
return payload, warnings
|
|
101
|
+
|
|
102
|
+
def semantic_namespace(self):
|
|
103
|
+
"""Return the verified account namespace, or require an explicit override."""
|
|
104
|
+
payload, _ = self.whoami_info()
|
|
105
|
+
value = payload.get("semantic_namespace")
|
|
106
|
+
if not value:
|
|
107
|
+
raise ValidationError(
|
|
108
|
+
"Cannot verify the main-account namespace for this identity. Pass --namespace with an independently verified main-account ID; do not use a RAM principal ID or project tenant ID."
|
|
109
|
+
)
|
|
110
|
+
return value
|
|
51
111
|
|
|
52
112
|
def _check_permission(
|
|
53
113
|
self,
|
|
54
114
|
*,
|
|
55
|
-
object_name:
|
|
56
|
-
object_type:
|
|
57
|
-
action:
|
|
58
|
-
project:
|
|
59
|
-
schema:
|
|
60
|
-
) ->
|
|
115
|
+
object_name: "str",
|
|
116
|
+
object_type: "str",
|
|
117
|
+
action: "str",
|
|
118
|
+
project: "str",
|
|
119
|
+
schema: "str | None" = None,
|
|
120
|
+
) -> "tuple[bool, str]":
|
|
61
121
|
"""Call the schema-aware MaxCompute checkPermission REST API.
|
|
62
122
|
|
|
63
123
|
This mirrors the current Java SDK ``SecurityManager#checkPermission``
|
|
@@ -128,11 +188,11 @@ class AuthMixin:
|
|
|
128
188
|
def _resolve_permission_target(
|
|
129
189
|
self,
|
|
130
190
|
*,
|
|
131
|
-
object_name:
|
|
132
|
-
object_type:
|
|
133
|
-
project:
|
|
134
|
-
schema:
|
|
135
|
-
) ->
|
|
191
|
+
object_name: "str",
|
|
192
|
+
object_type: "str",
|
|
193
|
+
project: "str | None",
|
|
194
|
+
schema: "str | None",
|
|
195
|
+
) -> "tuple[str, str, str | None, str]":
|
|
136
196
|
"""Resolve a permission target without relying on server error guesses.
|
|
137
197
|
|
|
138
198
|
Table-like names accept ``table``, ``schema.table`` and
|
|
@@ -233,12 +293,12 @@ class AuthMixin:
|
|
|
233
293
|
def can_i_info(
|
|
234
294
|
self,
|
|
235
295
|
*,
|
|
236
|
-
object_name:
|
|
237
|
-
object_type:
|
|
238
|
-
operation:
|
|
239
|
-
project:
|
|
240
|
-
schema:
|
|
241
|
-
) ->
|
|
296
|
+
object_name: "str",
|
|
297
|
+
object_type: "str" = "Table",
|
|
298
|
+
operation: "str",
|
|
299
|
+
project: "str | None" = None,
|
|
300
|
+
schema: "str | None" = None,
|
|
301
|
+
) -> "tuple[dict[str, Any], list[str]]":
|
|
242
302
|
"""Check if a specific operation is allowed on an object.
|
|
243
303
|
|
|
244
304
|
Calls the MaxCompute checkPermission REST API directly.
|
|
@@ -292,7 +352,7 @@ class AuthMixin:
|
|
|
292
352
|
[],
|
|
293
353
|
)
|
|
294
354
|
|
|
295
|
-
def _get_owner_display_name(self) ->
|
|
355
|
+
def _get_owner_display_name(self) -> "str | None":
|
|
296
356
|
"""Get the current user's display name (e.g., 'ALIYUN$xxx' or 'RAM$xxx')."""
|
|
297
357
|
if self._owner_display_name is not None:
|
|
298
358
|
return self._owner_display_name
|
|
@@ -24,6 +24,8 @@ import json
|
|
|
24
24
|
import logging
|
|
25
25
|
from typing import Any
|
|
26
26
|
|
|
27
|
+
from ..catalog_endpoint import normalize_catalog_endpoint
|
|
28
|
+
|
|
27
29
|
logger = logging.getLogger(__name__)
|
|
28
30
|
|
|
29
31
|
# KV store keys — scoped by project to handle multi-tenant setups
|
|
@@ -117,14 +119,16 @@ class CatalogMixin:
|
|
|
117
119
|
max_age_hours=_CACHE_TTL_HOURS,
|
|
118
120
|
)
|
|
119
121
|
if cached is not None:
|
|
120
|
-
self._catalog_ep_cached = cached
|
|
121
|
-
return
|
|
122
|
+
self._catalog_ep_cached = normalize_catalog_endpoint(cached)
|
|
123
|
+
return self._catalog_ep_cached
|
|
122
124
|
|
|
123
125
|
# Network call via pyodps auto-routing
|
|
124
126
|
try:
|
|
125
127
|
odps = self.client
|
|
126
128
|
if odps is not None:
|
|
127
129
|
ep = odps.catalog_endpoint
|
|
130
|
+
if ep is not None:
|
|
131
|
+
ep = normalize_catalog_endpoint(ep)
|
|
128
132
|
if ep is not None and cache is not None:
|
|
129
133
|
cache.set_kv(self._cache_key(_CATALOG_EP_KEY), ep)
|
|
130
134
|
self._catalog_ep_cached = ep
|
|
@@ -13,7 +13,7 @@ from ..helpers import (
|
|
|
13
13
|
translate_odps_error,
|
|
14
14
|
)
|
|
15
15
|
from ..models import QueryResult
|
|
16
|
-
from ..utils import detect_operation, extract_table_names
|
|
16
|
+
from ..utils import detect_operation, extract_table_names, sql_keyword_tokens, sql_statements
|
|
17
17
|
from .auth import AuthMixin
|
|
18
18
|
from .catalog import CatalogMixin
|
|
19
19
|
from .data import DataMixin
|
|
@@ -75,6 +75,26 @@ def _raw_task_result_rows(task_results: 'dict[str, Any]') -> 'list[dict[str, Any
|
|
|
75
75
|
]
|
|
76
76
|
|
|
77
77
|
|
|
78
|
+
def _is_show_partitions(sql: 'str') -> 'bool':
|
|
79
|
+
statements = sql_statements(sql)
|
|
80
|
+
return len(statements) == 1 and sql_keyword_tokens(statements[0])[:2] == ["SHOW", "PARTITIONS"]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _show_partition_rows(task_results: 'Any') -> 'list[dict[str, str]]':
|
|
84
|
+
"""SHOW PARTITIONS emits headerless lines, not a CSV table with a header."""
|
|
85
|
+
if not isinstance(task_results, dict):
|
|
86
|
+
raise ValueError("SHOW PARTITIONS returned invalid task results")
|
|
87
|
+
rows = []
|
|
88
|
+
for text in task_results.values():
|
|
89
|
+
if text is None:
|
|
90
|
+
continue
|
|
91
|
+
if not isinstance(text, str):
|
|
92
|
+
raise ValueError("SHOW PARTITIONS returned invalid task result text")
|
|
93
|
+
# Split only protocol line endings; Unicode separators may be values.
|
|
94
|
+
rows.extend({"partition": line.removesuffix("\r")} for line in text.split("\n") if line.removesuffix("\r"))
|
|
95
|
+
return rows
|
|
96
|
+
|
|
97
|
+
|
|
78
98
|
class OdpsBackend(
|
|
79
99
|
JobMixin, # JobMixin extends QueryMixin
|
|
80
100
|
CatalogMixin,
|
|
@@ -123,11 +143,12 @@ class OdpsBackend(
|
|
|
123
143
|
) -> 'QueryResult':
|
|
124
144
|
"""Convert ODPS instance to QueryResult."""
|
|
125
145
|
fallback_warnings: list[str] = []
|
|
146
|
+
show_partitions = _is_show_partitions(sql)
|
|
126
147
|
statement_operation = _write_operation(sql)
|
|
127
148
|
instance_is_select = getattr(instance, "_is_select", None)
|
|
128
149
|
confirmed_task_results = None
|
|
129
150
|
resultless_statement = statement_operation is not None and instance_is_select is not True
|
|
130
|
-
if not resultless_statement and instance_is_select is False:
|
|
151
|
+
if not show_partitions and not resultless_statement and instance_is_select is False:
|
|
131
152
|
try:
|
|
132
153
|
task_results = instance.get_task_results()
|
|
133
154
|
except Exception:
|
|
@@ -140,7 +161,23 @@ class OdpsBackend(
|
|
|
140
161
|
resultless_statement = not any(str(value or "").strip() for value in task_results.values())
|
|
141
162
|
if resultless_statement and sql.strip():
|
|
142
163
|
statement_operation = detect_operation(sql)
|
|
143
|
-
if
|
|
164
|
+
if show_partitions:
|
|
165
|
+
try:
|
|
166
|
+
# The generic CSV reader consumes the first partition as a
|
|
167
|
+
# header and raises on an empty body. Read this known textual
|
|
168
|
+
# result directly, retaining lookup failures as real errors.
|
|
169
|
+
all_rows = _show_partition_rows(instance.get_task_results())
|
|
170
|
+
except Exception as exc:
|
|
171
|
+
error = translate_odps_error(exc)
|
|
172
|
+
error.instance_id = instance.id
|
|
173
|
+
error.logview = self._safe_logview(instance)
|
|
174
|
+
raise error from exc
|
|
175
|
+
schema = [{"name": "partition", "type": "string", "comment": ""}]
|
|
176
|
+
rows = all_rows[offset : offset + max_rows]
|
|
177
|
+
total_rows = len(all_rows)
|
|
178
|
+
degraded_reason = None
|
|
179
|
+
resultless_statement = False
|
|
180
|
+
elif resultless_statement:
|
|
144
181
|
# Successful write statements have no tabular result. In
|
|
145
182
|
# particular, PyODPS's DDL fallback reader may try to iterate a
|
|
146
183
|
# missing task_results value and raise after the server job has
|
|
@@ -11,6 +11,7 @@ from urllib.parse import quote, urlsplit, urlunsplit
|
|
|
11
11
|
import requests
|
|
12
12
|
from odps import options
|
|
13
13
|
|
|
14
|
+
from ..catalog_endpoint import normalize_catalog_endpoint
|
|
14
15
|
from ..exceptions import MaxCError, ValidationError
|
|
15
16
|
from ..semantic import MAX_FILE_BYTES, SemanticError, identifier, namespace_id
|
|
16
17
|
|
|
@@ -31,12 +32,18 @@ def _failure(status, code, *, mutation):
|
|
|
31
32
|
"InvalidArgument": "VALIDATION_ERROR",
|
|
32
33
|
}
|
|
33
34
|
mapped = known.get(code) or {
|
|
34
|
-
400: "VALIDATION_ERROR",
|
|
35
|
-
|
|
35
|
+
400: "VALIDATION_ERROR",
|
|
36
|
+
401: "SEMANTIC_AUTHENTICATION_FAILED",
|
|
37
|
+
403: "SEMANTIC_PERMISSION_DENIED",
|
|
38
|
+
404: "SEMANTIC_NOT_FOUND",
|
|
36
39
|
409: "SEMANTIC_REVISION_CONFLICT",
|
|
37
40
|
}.get(status)
|
|
38
41
|
mapped = mapped or ("SEMANTIC_WRITE_UNCERTAIN" if mutation else "SEMANTIC_CONNECTION_ERROR")
|
|
39
|
-
return SemanticError(
|
|
42
|
+
return SemanticError(
|
|
43
|
+
mapped,
|
|
44
|
+
"Catalog semantic operation failed; inspect the object before retrying.",
|
|
45
|
+
context={"httpStatus": status},
|
|
46
|
+
)
|
|
40
47
|
|
|
41
48
|
|
|
42
49
|
class SemanticMixin:
|
|
@@ -47,11 +54,10 @@ class SemanticMixin:
|
|
|
47
54
|
try:
|
|
48
55
|
endpoint = self.client.catalog_rest.endpoint.rstrip("/")
|
|
49
56
|
except Exception as exc:
|
|
50
|
-
raise SemanticError(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return endpoint
|
|
57
|
+
raise SemanticError(
|
|
58
|
+
"SEMANTIC_CONNECTION_ERROR", "Catalog endpoint discovery failed."
|
|
59
|
+
) from exc
|
|
60
|
+
return normalize_catalog_endpoint(endpoint)
|
|
55
61
|
|
|
56
62
|
def semantic_request(self, namespace, method, name=None, *, suffix="", params=None, body=None):
|
|
57
63
|
namespace_id(namespace)
|
|
@@ -68,7 +74,9 @@ class SemanticMixin:
|
|
|
68
74
|
try:
|
|
69
75
|
payload = None
|
|
70
76
|
if body is not None:
|
|
71
|
-
payload = json.dumps(
|
|
77
|
+
payload = json.dumps(
|
|
78
|
+
body, ensure_ascii=False, separators=(",", ":"), allow_nan=False
|
|
79
|
+
).encode("utf-8")
|
|
72
80
|
if len(payload) > MAX_FILE_BYTES:
|
|
73
81
|
raise ValidationError("Semantic request exceeds the 16 MiB limit.")
|
|
74
82
|
# Reuse the account (including OAuth/STS/proxy behavior), region and
|
|
@@ -76,7 +84,9 @@ class SemanticMixin:
|
|
|
76
84
|
headers = {"Content-Type": "application/json", "Accept": "application/json"}
|
|
77
85
|
user_agent = getattr(rest, "_user_agent", "maxc-cli")
|
|
78
86
|
headers.update({"User-Agent": user_agent, "x-odps-user-agent": user_agent})
|
|
79
|
-
request = requests.Request(
|
|
87
|
+
request = requests.Request(
|
|
88
|
+
method, base + path, params=params or {}, data=payload, headers=headers
|
|
89
|
+
).prepare()
|
|
80
90
|
region = rest.region_name if options.enable_v4_sign else None
|
|
81
91
|
# PyODPS sign_request unquotes the URL before parse_qsl unquotes
|
|
82
92
|
# each query value again. Protect percent escapes for signing only,
|
|
@@ -94,14 +104,30 @@ class SemanticMixin:
|
|
|
94
104
|
# requests' default adapters have zero retries. No redirect can
|
|
95
105
|
# forward credentials or silently change a signed target/method.
|
|
96
106
|
with requests.Session() as session:
|
|
97
|
-
settings = session.merge_environment_settings(
|
|
98
|
-
|
|
107
|
+
settings = session.merge_environment_settings(
|
|
108
|
+
request.url,
|
|
109
|
+
dict(getattr(rest, "_proxy", None) or {}),
|
|
110
|
+
True,
|
|
111
|
+
options.verify_ssl,
|
|
112
|
+
None,
|
|
113
|
+
)
|
|
114
|
+
with session.send(
|
|
115
|
+
request,
|
|
116
|
+
timeout=(options.connect_timeout, options.read_timeout),
|
|
117
|
+
allow_redirects=False,
|
|
118
|
+
**settings,
|
|
119
|
+
) as response:
|
|
99
120
|
status = response.status_code
|
|
100
121
|
raw = bytearray()
|
|
101
122
|
for chunk in response.iter_content(64 * 1024):
|
|
102
123
|
raw.extend(chunk)
|
|
103
124
|
if len(raw) > MAX_FILE_BYTES:
|
|
104
|
-
raise SemanticError(
|
|
125
|
+
raise SemanticError(
|
|
126
|
+
"SEMANTIC_WRITE_UNCERTAIN"
|
|
127
|
+
if mutation
|
|
128
|
+
else "SEMANTIC_INVALID_RESPONSE",
|
|
129
|
+
"Catalog response exceeds the 16 MiB limit.",
|
|
130
|
+
)
|
|
105
131
|
if not 200 <= status < 300:
|
|
106
132
|
code = ""
|
|
107
133
|
try:
|
|
@@ -112,7 +138,11 @@ class SemanticMixin:
|
|
|
112
138
|
pass
|
|
113
139
|
raise _failure(status, code, mutation=mutation)
|
|
114
140
|
if method == "DELETE":
|
|
115
|
-
return {
|
|
141
|
+
return {
|
|
142
|
+
"deleted": True,
|
|
143
|
+
"specName": name,
|
|
144
|
+
"expectedSemanticSpecId": (params or {}).get("expectedSemanticSpecId"),
|
|
145
|
+
}
|
|
116
146
|
result = json.loads(raw)
|
|
117
147
|
if not isinstance(result, dict):
|
|
118
148
|
raise ValueError("expected object")
|
|
@@ -121,4 +151,7 @@ class SemanticMixin:
|
|
|
121
151
|
raise
|
|
122
152
|
except Exception as exc:
|
|
123
153
|
# Raw exceptions can contain signed headers, SQL, or user content.
|
|
124
|
-
raise SemanticError(
|
|
154
|
+
raise SemanticError(
|
|
155
|
+
"SEMANTIC_WRITE_UNCERTAIN" if mutation else "SEMANTIC_CONNECTION_ERROR",
|
|
156
|
+
"Catalog semantic request did not produce a verifiable result. Reconcile before retrying a write.",
|
|
157
|
+
) from exc
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Normalize SDK-discovered Catalog authorities without changing explicit routing."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from urllib.parse import urlsplit
|
|
5
|
+
|
|
6
|
+
from .exceptions import ValidationError
|
|
7
|
+
|
|
8
|
+
_BARE_AUTHORITY = re.compile(
|
|
9
|
+
r"(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+"
|
|
10
|
+
r"[A-Za-z0-9-]+(?::[0-9]{1,5})?"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def normalize_catalog_endpoint(endpoint):
|
|
15
|
+
if not isinstance(endpoint, str) or not endpoint:
|
|
16
|
+
raise ValidationError("Catalog endpoint must be a nonempty HTTP(S) service URL.")
|
|
17
|
+
endpoint = endpoint.rstrip("/")
|
|
18
|
+
if _BARE_AUTHORITY.fullmatch(endpoint):
|
|
19
|
+
endpoint = "https://" + endpoint
|
|
20
|
+
try:
|
|
21
|
+
parsed = urlsplit(endpoint)
|
|
22
|
+
valid = (
|
|
23
|
+
parsed.scheme in ("https", "http")
|
|
24
|
+
and parsed.hostname
|
|
25
|
+
and not parsed.username
|
|
26
|
+
and not parsed.password
|
|
27
|
+
and not parsed.query
|
|
28
|
+
and not parsed.fragment
|
|
29
|
+
and not any(c.isspace() for c in endpoint)
|
|
30
|
+
and "\\" not in endpoint
|
|
31
|
+
and "%" not in parsed.hostname
|
|
32
|
+
)
|
|
33
|
+
# Accessing port checks malformed/non-numeric/out-of-range values.
|
|
34
|
+
if parsed.port is not None and not 1 <= parsed.port <= 65535:
|
|
35
|
+
valid = False
|
|
36
|
+
except ValueError:
|
|
37
|
+
valid = False
|
|
38
|
+
if not valid:
|
|
39
|
+
raise ValidationError(
|
|
40
|
+
"Catalog endpoint must be an HTTP(S) service URL without credentials, query, or fragment."
|
|
41
|
+
)
|
|
42
|
+
return endpoint
|
|
@@ -453,6 +453,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
453
453
|
meta_describe.add_argument("--project", help="Target MaxCompute project")
|
|
454
454
|
meta_describe.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
455
455
|
meta_describe.add_argument("--full", action="store_true", help="Show full column list (default is summary mode)")
|
|
456
|
+
meta_describe.add_argument("--metadata-only", action="store_true", help="Read live table schema only, without rows, partition values or cache fallback")
|
|
456
457
|
meta_describe.set_defaults(handler=_handle_meta_describe)
|
|
457
458
|
|
|
458
459
|
meta_functions = _make_parser(meta_subparsers, "list-functions", "meta.list-functions", help="List registered functions")
|
|
@@ -1158,7 +1159,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1158
1159
|
child = _make_parser(semantic_commands, operation, "semantic." + operation, help=summary)
|
|
1159
1160
|
if operation != "list":
|
|
1160
1161
|
child.add_argument("name", help="Exact semantic package name")
|
|
1161
|
-
child.add_argument("--namespace",
|
|
1162
|
+
child.add_argument("--namespace", help="Verified main-account ID; auto-discovered from authenticated account identity when omitted")
|
|
1162
1163
|
child.add_argument("--json", action="store_true", help="Output JSON envelope")
|
|
1163
1164
|
if operation == "list":
|
|
1164
1165
|
child.add_argument("--page-size", type=positive_int, default=20)
|
|
@@ -2152,6 +2153,7 @@ def _handle_meta_describe(app: MaxCApp, args: argparse.Namespace, stdout: TextIO
|
|
|
2152
2153
|
full = args.full or _is_json_mode(args)
|
|
2153
2154
|
envelope = app.meta_describe(
|
|
2154
2155
|
args.table_name,
|
|
2156
|
+
metadata_only=getattr(args, "metadata_only", False),
|
|
2155
2157
|
full=full,
|
|
2156
2158
|
project=args.project,
|
|
2157
2159
|
schema=getattr(args, "schema", None),
|
|
@@ -2940,7 +2942,7 @@ def _manifest_effects(command: str) -> list[dict[str, Any]]:
|
|
|
2940
2942
|
when=_manifest_condition("yes", equals=True), confirmation="--yes with exact target/version; apply also requires --plan-digest"))
|
|
2941
2943
|
if operation in {"create", "diff", "apply"}:
|
|
2942
2944
|
effects.append(_manifest_effect("local", "read", "semantic_json_file"))
|
|
2943
|
-
if operation
|
|
2945
|
+
if operation in {"export"}:
|
|
2944
2946
|
effects.append(_manifest_effect("local", "create", "semantic_json_file", when=_manifest_condition("overwrite", equals=False)))
|
|
2945
2947
|
effects.append(_manifest_effect("local", "replace", "semantic_json_file", when=_manifest_condition("overwrite", equals=True), confirmation="--overwrite"))
|
|
2946
2948
|
return _with_manifest_audit_effect(command, effects)
|
|
@@ -796,7 +796,7 @@ def load_config(cwd: 'Path', explicit_path: 'Path | None' = None) -> 'MaxCConfig
|
|
|
796
796
|
]
|
|
797
797
|
cost_threshold_cu = float(merged.get("cost_threshold_cu", 50))
|
|
798
798
|
masking_cfg = merged.get("masking", {}) or {}
|
|
799
|
-
masking_enabled = bool(masking_cfg.get("enabled",
|
|
799
|
+
masking_enabled = bool(masking_cfg.get("enabled", False))
|
|
800
800
|
sensitive_columns = [str(item) for item in merged.get("sensitive_columns", [])]
|
|
801
801
|
sensitive_columns += [str(item) for item in masking_cfg.get("sensitive_patterns", [])]
|
|
802
802
|
|
|
@@ -19,7 +19,7 @@ from .semantic import (
|
|
|
19
19
|
class SemanticManager:
|
|
20
20
|
def __init__(self, backend, namespace):
|
|
21
21
|
self.backend = backend
|
|
22
|
-
self.namespace = namespace_id(namespace)
|
|
22
|
+
self.namespace = namespace_id(namespace if namespace is not None else backend.semantic_namespace())
|
|
23
23
|
self.warnings = []
|
|
24
24
|
|
|
25
25
|
def request(self, method, name=None, **kwargs):
|
|
@@ -6,8 +6,11 @@ Read the live help/manifest before choosing arguments.
|
|
|
6
6
|
|
|
7
7
|
## Discover and read
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
Omit `--namespace` to resolve it from verified authenticated account identity.
|
|
10
|
+
If discovery cannot prove the parent account, pass an independently verified
|
|
11
|
+
main-account ID; neither a standalone RAM principal ID nor a project tenant ID
|
|
12
|
+
is interchangeable with this namespace. `auth whoami` exposes the resolution
|
|
13
|
+
under `data.identity.semantic_namespace` and its source. The active authentication and Catalog endpoint
|
|
11
14
|
select the caller and region. Follow `nextPageToken` with `--page-token` until
|
|
12
15
|
the intended exact name is found. Authorization remains server-enforced.
|
|
13
16
|
|