maxc-cli 0.7.0__tar.gz → 0.8.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.7.0 → maxc_cli-0.8.1}/PKG-INFO +16 -1
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/README.md +15 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/__init__.py +1 -1
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/_samples.py +8 -3
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/app.py +114 -12
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/auth_providers.py +19 -3
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/backend/job.py +66 -1
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/cli.py +106 -8
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/config.py +2 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/enterprise_tls.py +44 -22
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/helpers.py +18 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/oauth.py +12 -5
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/SKILL.md +3 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/bootstrap-auth.md +22 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/command-patterns.md +27 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli.egg-info/PKG-INFO +16 -1
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli.egg-info/SOURCES.txt +2 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_agent_skill_commands_context.py +25 -23
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_cache.py +30 -16
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_cli_mock.py +3 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_config_atomic_write.py +2 -0
- maxc_cli-0.8.1/tests/test_connection_bootstrap.py +279 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_enterprise_tls.py +109 -1
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_external_auth.py +4 -4
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_installer_contracts.py +5 -0
- maxc_cli-0.8.1/tests/test_job_inspection.py +160 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_job_store_durability.py +9 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_oauth.py +41 -5
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_output_format_contract.py +2 -1
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_semantic_management.py +1 -1
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/MANIFEST.in +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/pyproject.toml +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/scripts/pyinstaller_entry.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/scripts/regression_test.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/setup.cfg +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/setup.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/__main__.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/agent_platforms.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/audit.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/auth_continuation.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/backend/__init__.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/backend/auth.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/backend/catalog.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/backend/data.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/backend/mcp.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/backend/meta.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/backend/odps.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/backend/query.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/backend/semantic.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/cache.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/catalog_bootstrap.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/exceptions.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/help_format.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/job_ids.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/masking.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/mcp_serve.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/models.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/odps_runtime.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/output.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/proxy_auth.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/semantic.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/semantic_management.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/setting_parser.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/agents/openai.yaml +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/bootstrap-flow.md +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/json-output-format.md +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/maxcompute-select-guide.md +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/maxcompute-sql-notes.md +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/partition-guide.md +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/red-lines.md +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/semantic-packages.md +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/setup-install.md +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/sql-common-errors.md +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/sql-query-patterns.md +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/text2sql-principles.md +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/state_permissions.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/store.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/utils.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli.egg-info/dependency_links.txt +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli.egg-info/entry_points.txt +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli.egg-info/requires.txt +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli.egg-info/top_level.txt +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_agent_hints_and_cli.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_agent_platforms.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_agent_skill_commands.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_ai_native_contract_regressions.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_auth_logout.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_backend_auth.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_backend_data.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_backend_data_serialization.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_backend_mcp.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_backend_meta.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_build_release_archive_compat.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_build_release_script.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_catalog.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_catalog_bootstrap.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_cli_arg_validation.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_cli_query_parse_and_sanitize.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_compat.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_e2e_smoke.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_effective_hints_contract.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_envelope_shape.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_error_self_correction.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_error_translation.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_exit_codes.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_flag_hoist.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_help_format.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_help_version_e2e.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_helpers.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_helpers_csv.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_integration.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_integration_real.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_job_improvements.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_kb_commands.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_manifest_runtime_contract.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_masking.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_mcp_serve.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_meta_schema_and_partition_cols.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_odps_runtime.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_output_action_safety.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_packaging_metadata.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_phase1_improvements.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_proxy_auth.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_pyinstaller_bundle.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_python39_compat.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_query_auto_promote.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_query_result_csv_fallback.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_semantic_scope.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_semantic_transport.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_setting_parser.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_skill_cli_consistency.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_skill_eval.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_skill_renderer.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_startup_imports.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_state_permissions.py +0 -0
- {maxc_cli-0.7.0 → maxc_cli-0.8.1}/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.
|
|
3
|
+
Version: 0.8.1
|
|
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
|
|
@@ -311,3 +311,18 @@ Metadata edits lack server CAS; publication lacks idempotency keys. An uncertain
|
|
|
311
311
|
write must be reconciled before retrying. Publication checks are not SQL or
|
|
312
312
|
business validation. History lists the latest 20 summaries, and an immutable
|
|
313
313
|
revision read does not guarantee historical DataBridge analysis.
|
|
314
|
+
|
|
315
|
+
### Task metrics and worker logs
|
|
316
|
+
|
|
317
|
+
```bash
|
|
318
|
+
aliyun maxc job task-detail <instance_id> --task-name AnonymousSQLTask --json
|
|
319
|
+
aliyun maxc job task-summary <instance_id> --task-name AnonymousSQLTask --json
|
|
320
|
+
aliyun maxc job workers <instance_id> --task-name AnonymousSQLTask --json
|
|
321
|
+
aliyun maxc job worker-log <instance_id> <log_id> --log-type stdout --size 1048576 --json
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
These read-only commands use the same project and saved job context as `job status`.
|
|
325
|
+
Task detail preserves the service payload (including `mapReduce.jsonSummary`).
|
|
326
|
+
Select `log_id` from `data.workers`; logs are returned as `data.content` in the JSON
|
|
327
|
+
envelope. The requested size defaults to 1 MiB and must be positive; returned logs
|
|
328
|
+
may be partial. Use `--project` to select the owning project explicitly.
|
|
@@ -289,3 +289,18 @@ Metadata edits lack server CAS; publication lacks idempotency keys. An uncertain
|
|
|
289
289
|
write must be reconciled before retrying. Publication checks are not SQL or
|
|
290
290
|
business validation. History lists the latest 20 summaries, and an immutable
|
|
291
291
|
revision read does not guarantee historical DataBridge analysis.
|
|
292
|
+
|
|
293
|
+
### Task metrics and worker logs
|
|
294
|
+
|
|
295
|
+
```bash
|
|
296
|
+
aliyun maxc job task-detail <instance_id> --task-name AnonymousSQLTask --json
|
|
297
|
+
aliyun maxc job task-summary <instance_id> --task-name AnonymousSQLTask --json
|
|
298
|
+
aliyun maxc job workers <instance_id> --task-name AnonymousSQLTask --json
|
|
299
|
+
aliyun maxc job worker-log <instance_id> <log_id> --log-type stdout --size 1048576 --json
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
These read-only commands use the same project and saved job context as `job status`.
|
|
303
|
+
Task detail preserves the service payload (including `mapReduce.jsonSummary`).
|
|
304
|
+
Select `log_id` from `data.workers`; logs are returned as `data.content` in the JSON
|
|
305
|
+
envelope. The requested size defaults to 1 MiB and must be positive; returned logs
|
|
306
|
+
may be partial. Use `--project` to select the owning project explicitly.
|
|
@@ -9,7 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
SAMPLES: dict[str, str] = {
|
|
10
10
|
"__top__": (
|
|
11
11
|
"maxc auth login\n"
|
|
12
|
-
'maxc query "SELECT 1"\n'
|
|
12
|
+
'maxc query "SELECT 1" --project my_proj --json\n'
|
|
13
13
|
"maxc meta list-tables --project my_proj"
|
|
14
14
|
),
|
|
15
15
|
"semantic.list": "maxc semantic list --namespace <account_id> --json",
|
|
@@ -23,7 +23,7 @@ SAMPLES: dict[str, str] = {
|
|
|
23
23
|
"semantic.delete": "maxc semantic delete sales --namespace <account_id> --expected-spec-id <spec_id> --json",
|
|
24
24
|
# ── query ──────────────────────────────────────────────────────────────
|
|
25
25
|
"query": (
|
|
26
|
-
'maxc query "SELECT 1"\n'
|
|
26
|
+
'maxc query "SELECT 1" --project my_proj --json\n'
|
|
27
27
|
"maxc query cost \"SELECT * FROM default.big_table WHERE ds='20260101'\"\n"
|
|
28
28
|
"maxc query explain \"SELECT * FROM default.events WHERE dt='20260101'\""
|
|
29
29
|
),
|
|
@@ -31,7 +31,8 @@ SAMPLES: dict[str, str] = {
|
|
|
31
31
|
"auth": "maxc auth login --oauth\nmaxc auth whoami --json\nmaxc auth logout --json",
|
|
32
32
|
"auth.login": (
|
|
33
33
|
"maxc auth login --oauth\n"
|
|
34
|
-
"maxc auth login --from-env --json"
|
|
34
|
+
"maxc auth login --from-env --json\n"
|
|
35
|
+
"maxc auth login --reuse-auth --project my_proj --region cn-hangzhou --json"
|
|
35
36
|
),
|
|
36
37
|
"auth.login-proxy": (
|
|
37
38
|
"maxc auth login-proxy --project <project> --odps-endpoint <endpoint> --json",
|
|
@@ -59,6 +60,10 @@ SAMPLES: dict[str, str] = {
|
|
|
59
60
|
"maxc job wait <job_id>\n"
|
|
60
61
|
"maxc job wait <job_id> --timeout 600 --stream"
|
|
61
62
|
),
|
|
63
|
+
"job.task-detail": "maxc job task-detail <job_id> --task-name AnonymousSQLTask --json",
|
|
64
|
+
"job.task-summary": "maxc job task-summary <job_id> --task-name AnonymousSQLTask --json",
|
|
65
|
+
"job.workers": "maxc job workers <job_id> --task-name AnonymousSQLTask --json",
|
|
66
|
+
"job.worker-log": "maxc job worker-log <job_id> <log_id> --log-type stdout --size 1048576 --json",
|
|
62
67
|
"job.diagnose": "maxc job diagnose <job_id>\nmaxc job diagnose <job_id> --json",
|
|
63
68
|
"job.result": (
|
|
64
69
|
"maxc job result <job_id>\n"
|
|
@@ -158,6 +158,7 @@ class _PickerInputs:
|
|
|
158
158
|
env_settings: 'dict[str, str]'
|
|
159
159
|
existing_auth: 'AuthConfig'
|
|
160
160
|
reselect: 'bool' = False
|
|
161
|
+
allow_interactive: 'bool' = True
|
|
161
162
|
|
|
162
163
|
|
|
163
164
|
@dataclass(frozen=True)
|
|
@@ -193,9 +194,15 @@ class MaxCApp:
|
|
|
193
194
|
cwd: 'Path',
|
|
194
195
|
config_path: 'Path | None' = None,
|
|
195
196
|
load_backend: 'bool' = True,
|
|
197
|
+
project_override: 'str | None' = None,
|
|
196
198
|
) -> 'None':
|
|
197
199
|
self.cwd = cwd
|
|
198
200
|
self.config = load_config(cwd, config_path)
|
|
201
|
+
# Request-level routing must be applied before backend authentication
|
|
202
|
+
# validates the connection. It is never persisted as a login default.
|
|
203
|
+
if project_override:
|
|
204
|
+
self.config.command_project = project_override
|
|
205
|
+
self.config.default_project = project_override
|
|
199
206
|
self._cache: LocalCache | None = None
|
|
200
207
|
# Credential/catalog providers may need the cache later, but ordinary
|
|
201
208
|
# authenticated commands must not create or migrate SQLite merely by
|
|
@@ -1811,6 +1818,30 @@ class MaxCApp:
|
|
|
1811
1818
|
self.log("job.cancel", envelope.status, envelope.metadata)
|
|
1812
1819
|
return envelope
|
|
1813
1820
|
|
|
1821
|
+
def job_inspect(
|
|
1822
|
+
self, job_id: 'str', *, section: 'str', project: 'str | None' = None,
|
|
1823
|
+
task_name: 'str | None' = None, log_id: 'str | None' = None,
|
|
1824
|
+
log_type: 'str' = "stdout", size: 'int' = 1048576,
|
|
1825
|
+
) -> 'Envelope':
|
|
1826
|
+
if not self.remote_jobs:
|
|
1827
|
+
raise FeatureUnavailableError("Task diagnostics require a MaxCompute backend.")
|
|
1828
|
+
resolved = self._resolve_remote_job_id(job_id, project=project)
|
|
1829
|
+
payload = self.backend.inspect_job(
|
|
1830
|
+
resolved.instance_id, section=section, project=resolved.project,
|
|
1831
|
+
session_context=resolved.session_context, task_name=task_name,
|
|
1832
|
+
log_id=log_id, log_type=log_type, size=size,
|
|
1833
|
+
)
|
|
1834
|
+
envelope = Envelope(
|
|
1835
|
+
command=f"job.{section}", status="success",
|
|
1836
|
+
data={"job_id": resolved.external_job_id, **payload},
|
|
1837
|
+
metadata={"project": resolved.project, "job_id": resolved.external_job_id},
|
|
1838
|
+
agent_hints=AgentHints(warnings=[
|
|
1839
|
+
"The service may return only part of the log; requested_size does not prove completeness."
|
|
1840
|
+
] if section == "worker-log" else []),
|
|
1841
|
+
)
|
|
1842
|
+
self.log(envelope.command, envelope.status, envelope.metadata)
|
|
1843
|
+
return envelope
|
|
1844
|
+
|
|
1814
1845
|
def job_diagnose(self, job_id: 'str', *, project: 'str | None' = None) -> 'Envelope':
|
|
1815
1846
|
if self.remote_jobs:
|
|
1816
1847
|
resolved = self._resolve_remote_job_id(job_id, project=project)
|
|
@@ -4684,6 +4715,7 @@ class MaxCApp:
|
|
|
4684
4715
|
projects: 'list[dict[str, Any]]',
|
|
4685
4716
|
*,
|
|
4686
4717
|
title_prefix: 'str',
|
|
4718
|
+
endpoint_flag: 'str' = "--endpoint",
|
|
4687
4719
|
) -> 'list[SuggestedAction]':
|
|
4688
4720
|
actions: list[SuggestedAction] = []
|
|
4689
4721
|
for project_data in projects:
|
|
@@ -4693,7 +4725,7 @@ class MaxCApp:
|
|
|
4693
4725
|
tokens = [*base_tokens, "--project", project_id]
|
|
4694
4726
|
endpoint_value = project_data.get("endpoint")
|
|
4695
4727
|
endpoint_placeholder = endpoint_value is None
|
|
4696
|
-
tokens.extend([
|
|
4728
|
+
tokens.extend([endpoint_flag, str(endpoint_value or "__MAXC_ENDPOINT__")])
|
|
4697
4729
|
if project_data.get("region"):
|
|
4698
4730
|
tokens.extend(["--region", str(project_data["region"])])
|
|
4699
4731
|
if project_data.get("tunnel_endpoint"):
|
|
@@ -4731,6 +4763,8 @@ class MaxCApp:
|
|
|
4731
4763
|
region_name: 'str | None' = None,
|
|
4732
4764
|
tunnel_endpoint: 'str | None' = None,
|
|
4733
4765
|
from_env: 'bool' = False,
|
|
4766
|
+
reuse_auth: 'bool' = False,
|
|
4767
|
+
allow_interactive_picker: 'bool' = True,
|
|
4734
4768
|
no_validate: 'bool' = False,
|
|
4735
4769
|
target_config_path: 'Path | None' = None,
|
|
4736
4770
|
catalog_endpoint: 'str | None' = None,
|
|
@@ -4744,8 +4778,39 @@ class MaxCApp:
|
|
|
4744
4778
|
existing_auth = AuthConfig.from_mapping(existing_payload.get("auth", {}) or {})
|
|
4745
4779
|
env_settings = load_odps_env()
|
|
4746
4780
|
|
|
4747
|
-
#
|
|
4748
|
-
|
|
4781
|
+
# Connection setup reuses the effective identity, including the
|
|
4782
|
+
# wrapper's refreshable profile, without importing its temporary keys.
|
|
4783
|
+
if reuse_auth:
|
|
4784
|
+
if any((access_id, secret_access_key, security_token, continuation_id)) or from_env or _oauth_config:
|
|
4785
|
+
raise ValidationError("--reuse-auth cannot replace credentials or resume a login.")
|
|
4786
|
+
effective, _, _ = resolve_odps_settings(self.config)
|
|
4787
|
+
provider = infer_auth_provider(self.config, effective)
|
|
4788
|
+
resolved_access_id = effective.get("access_id")
|
|
4789
|
+
resolved_secret = effective.get("secret_access_key")
|
|
4790
|
+
resolved_token = effective.get("security_token")
|
|
4791
|
+
if provider == "oauth":
|
|
4792
|
+
from .auth_providers import _oauth_persist_path
|
|
4793
|
+
from .oauth import ensure_oauth_sts
|
|
4794
|
+
|
|
4795
|
+
sts = ensure_oauth_sts(self.config.auth, config_path=_oauth_persist_path(self.config))
|
|
4796
|
+
resolved_access_id = sts.access_key_id
|
|
4797
|
+
resolved_secret = sts.access_key_secret
|
|
4798
|
+
resolved_token = sts.security_token
|
|
4799
|
+
if provider == "sts_token" and not resolved_token:
|
|
4800
|
+
raise ValidationError(
|
|
4801
|
+
"The existing STS identity is missing its security token.",
|
|
4802
|
+
suggestion="Restore the existing STS profile before configuring the connection.",
|
|
4803
|
+
)
|
|
4804
|
+
if not resolved_access_id or not resolved_secret:
|
|
4805
|
+
raise ValidationError(
|
|
4806
|
+
"No reusable AccessKey/STS/OAuth credentials are available.",
|
|
4807
|
+
suggestion="Configure the intended identity before selecting a project.",
|
|
4808
|
+
)
|
|
4809
|
+
# Preserve the active provider's suppression of unrelated shell
|
|
4810
|
+
# values. Routing defaults below come from its effective settings.
|
|
4811
|
+
env_settings = effective
|
|
4812
|
+
existing_auth = AuthConfig.from_mapping(self.config.auth.to_mapping())
|
|
4813
|
+
elif continuation_id:
|
|
4749
4814
|
if any((access_id, secret_access_key, security_token)) or from_env:
|
|
4750
4815
|
raise ValidationError(
|
|
4751
4816
|
"Do not combine an auth continuation with credential flags or --from-env.",
|
|
@@ -4824,6 +4889,7 @@ class MaxCApp:
|
|
|
4824
4889
|
env_settings=env_settings,
|
|
4825
4890
|
existing_auth=existing_auth,
|
|
4826
4891
|
reselect=reselect,
|
|
4892
|
+
allow_interactive=allow_interactive_picker,
|
|
4827
4893
|
)
|
|
4828
4894
|
)
|
|
4829
4895
|
except ProjectPickerPending as exc:
|
|
@@ -4871,6 +4937,18 @@ class MaxCApp:
|
|
|
4871
4937
|
warnings=[],
|
|
4872
4938
|
),
|
|
4873
4939
|
)
|
|
4940
|
+
if reuse_auth:
|
|
4941
|
+
base_tokens = self._auth_action_prefix(target_path)
|
|
4942
|
+
base_tokens.extend(["auth", "login", "--reuse-auth"])
|
|
4943
|
+
if catalog_endpoint:
|
|
4944
|
+
base_tokens.extend(["--catalog-endpoint", catalog_endpoint])
|
|
4945
|
+
if no_validate:
|
|
4946
|
+
base_tokens.append("--no-validate")
|
|
4947
|
+
pending_envelope.agent_hints.actions = self._project_selection_actions(
|
|
4948
|
+
base_tokens, projects_data, title_prefix="Configure connection for",
|
|
4949
|
+
endpoint_flag="--odps-endpoint",
|
|
4950
|
+
)
|
|
4951
|
+
return pending_envelope
|
|
4874
4952
|
if _oauth_config is not None:
|
|
4875
4953
|
return pending_envelope
|
|
4876
4954
|
if continuation_id:
|
|
@@ -4910,6 +4988,15 @@ class MaxCApp:
|
|
|
4910
4988
|
pending_envelope.metadata["continuation_expires_at_unix"] = expires_at
|
|
4911
4989
|
return pending_envelope
|
|
4912
4990
|
|
|
4991
|
+
from .catalog_bootstrap import region_to_endpoint, region_to_tunnel_endpoint
|
|
4992
|
+
|
|
4993
|
+
fallback_region = derived_region or env_settings.get("region_name") or existing_auth.region_name
|
|
4994
|
+
derived_region = fallback_region
|
|
4995
|
+
if not derived_endpoint and not env_settings.get("endpoint") and not existing_auth.endpoint:
|
|
4996
|
+
derived_endpoint = region_to_endpoint(fallback_region)
|
|
4997
|
+
if not derived_tunnel and not env_settings.get("tunnel_endpoint") and not existing_auth.tunnel_endpoint:
|
|
4998
|
+
derived_tunnel = region_to_tunnel_endpoint(fallback_region)
|
|
4999
|
+
|
|
4913
5000
|
resolved_auth = AuthConfig(
|
|
4914
5001
|
access_id=resolved_access_id,
|
|
4915
5002
|
secret_access_key=resolved_secret,
|
|
@@ -4959,10 +5046,25 @@ class MaxCApp:
|
|
|
4959
5046
|
persisted_auth.provider = "oauth"
|
|
4960
5047
|
persisted_auth.oauth = _oauth_config
|
|
4961
5048
|
migrate_legacy_session_override(target_path)
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
5049
|
+
if reuse_auth:
|
|
5050
|
+
connection_fields = {
|
|
5051
|
+
name: getattr(resolved_auth, name)
|
|
5052
|
+
for name in ("project", "endpoint", "region_name", "tunnel_endpoint")
|
|
5053
|
+
if getattr(resolved_auth, name)
|
|
5054
|
+
}
|
|
5055
|
+
|
|
5056
|
+
def save_connection(payload: 'dict[str, Any]') -> 'None':
|
|
5057
|
+
auth_mapping = payload.setdefault("auth", {})
|
|
5058
|
+
if not isinstance(auth_mapping, dict):
|
|
5059
|
+
raise ValidationError("The auth configuration must be a mapping.")
|
|
5060
|
+
auth_mapping.update(connection_fields)
|
|
5061
|
+
payload["default_project"] = resolved_auth.project
|
|
5062
|
+
if resolved_auth.region_name:
|
|
5063
|
+
payload["default_region"] = resolved_auth.region_name
|
|
5064
|
+
|
|
5065
|
+
update_config_mapping(target_path, save_connection)
|
|
5066
|
+
else:
|
|
5067
|
+
persist_login_config(target_path, auth=persisted_auth)
|
|
4966
5068
|
|
|
4967
5069
|
continuation_cleanup_warning: str | None = None
|
|
4968
5070
|
if continuation_id:
|
|
@@ -4983,7 +5085,7 @@ class MaxCApp:
|
|
|
4983
5085
|
# Always remind callers that AK/SK is stored in plaintext YAML (chmod
|
|
4984
5086
|
# 0600) — flagged in CLAUDE.md as a known limitation. Skip for STS
|
|
4985
5087
|
# tokens since those are short-lived and self-expiring.
|
|
4986
|
-
if not resolved_auth.security_token:
|
|
5088
|
+
if not reuse_auth and not resolved_auth.security_token:
|
|
4987
5089
|
warnings.append(
|
|
4988
5090
|
f"AccessKey saved in plaintext at `{target_path}` (file mode 0600). "
|
|
4989
5091
|
f"For shared/CI environments prefer `auth login-external` with a credential helper, "
|
|
@@ -4993,7 +5095,7 @@ class MaxCApp:
|
|
|
4993
5095
|
warnings.append(
|
|
4994
5096
|
"Credentials were imported from environment variables (--from-env) and saved to config."
|
|
4995
5097
|
)
|
|
4996
|
-
elif any(
|
|
5098
|
+
elif not reuse_auth and any(
|
|
4997
5099
|
env_settings.get(name)
|
|
4998
5100
|
for name in ("access_id", "secret_access_key", "security_token", "endpoint", "region_name", "tunnel_endpoint")
|
|
4999
5101
|
):
|
|
@@ -5031,8 +5133,8 @@ class MaxCApp:
|
|
|
5031
5133
|
|
|
5032
5134
|
login_metadata = {
|
|
5033
5135
|
"config_path": str(target_path),
|
|
5034
|
-
"written_fields": sorted(persisted_auth.to_mapping().keys()),
|
|
5035
|
-
"auth_storage": "config_file",
|
|
5136
|
+
"written_fields": sorted(connection_fields if reuse_auth else persisted_auth.to_mapping().keys()),
|
|
5137
|
+
"auth_storage": "existing_identity" if reuse_auth else "config_file",
|
|
5036
5138
|
}
|
|
5037
5139
|
envelope = Envelope(
|
|
5038
5140
|
command="auth.login",
|
|
@@ -5726,7 +5828,7 @@ class MaxCApp:
|
|
|
5726
5828
|
from . import catalog_bootstrap as _catalog_bootstrap
|
|
5727
5829
|
|
|
5728
5830
|
# 2. Picker not viable (non-TTY or --no-picker).
|
|
5729
|
-
if inputs.no_picker or not sys.stdin.isatty():
|
|
5831
|
+
if inputs.no_picker or not inputs.allow_interactive or not sys.stdin.isatty():
|
|
5730
5832
|
# Non-TTY + picker not disabled: list projects for structured output
|
|
5731
5833
|
catalog_warning: str | None = None
|
|
5732
5834
|
if not inputs.no_picker and not from_env:
|
|
@@ -259,7 +259,14 @@ def resolve_auth_connection(
|
|
|
259
259
|
if missing:
|
|
260
260
|
raise ValidationError(
|
|
261
261
|
f"STS authentication is missing required fields: {', '.join(missing)}.",
|
|
262
|
-
suggestion=
|
|
262
|
+
suggestion=(
|
|
263
|
+
"Pass `--project <project>` on the data command; "
|
|
264
|
+
"or specify `maxc auth login --reuse-auth --project <project> "
|
|
265
|
+
"--odps-endpoint <endpoint>`."
|
|
266
|
+
if set(missing) <= {"project", "endpoint"}
|
|
267
|
+
else "Provide access_id, secret_access_key, security_token, project, and endpoint."
|
|
268
|
+
),
|
|
269
|
+
context={"missing_fields": missing},
|
|
263
270
|
)
|
|
264
271
|
try:
|
|
265
272
|
from odps.accounts import StsAccount
|
|
@@ -303,7 +310,10 @@ def resolve_auth_connection(
|
|
|
303
310
|
if not (settings.get("project") or config.default_project) or not settings.get("endpoint"):
|
|
304
311
|
raise ValidationError(
|
|
305
312
|
"OAuth login is incomplete: project or endpoint missing.",
|
|
306
|
-
suggestion="
|
|
313
|
+
suggestion="Pass `--project <project>` on the data command, or use `maxc auth login --reuse-auth` to configure a default.",
|
|
314
|
+
context={"missing_fields": [
|
|
315
|
+
name for name in ("project", "endpoint") if not settings.get(name)
|
|
316
|
+
]},
|
|
307
317
|
)
|
|
308
318
|
try:
|
|
309
319
|
from odps.accounts import StsAccount
|
|
@@ -334,7 +344,13 @@ def resolve_auth_connection(
|
|
|
334
344
|
if missing:
|
|
335
345
|
raise ValidationError(
|
|
336
346
|
f"MaxCompute connection settings are incomplete: {', '.join(missing)}.",
|
|
337
|
-
suggestion=
|
|
347
|
+
suggestion=(
|
|
348
|
+
"Pass `--project <project>` on the data command; "
|
|
349
|
+
"or specify --project <project> --odps-endpoint <endpoint>."
|
|
350
|
+
if set(missing) <= {"project", "endpoint"}
|
|
351
|
+
else "Run `maxc auth login` or set the required environment variables."
|
|
352
|
+
),
|
|
353
|
+
context={"missing_fields": missing},
|
|
338
354
|
)
|
|
339
355
|
return ResolvedAuthConnection(
|
|
340
356
|
auth_type="access_key",
|
|
@@ -5,7 +5,7 @@ from itertools import islice
|
|
|
5
5
|
from time import monotonic, sleep
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
|
-
from ..exceptions import BackendConnectionError, JobTimeoutError, ValidationError
|
|
8
|
+
from ..exceptions import BackendConnectionError, JobTimeoutError, MaxCError, ValidationError
|
|
9
9
|
from ..helpers import (
|
|
10
10
|
OdpsNoSuchObject,
|
|
11
11
|
_dt_to_iso,
|
|
@@ -301,6 +301,71 @@ class JobMixin(QueryMixin):
|
|
|
301
301
|
"task_results": task_results,
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
+
def inspect_job(
|
|
305
|
+
self, job_id: 'str', *, section: 'str', project: 'str | None' = None,
|
|
306
|
+
task_name: 'str | None' = None, log_id: 'str | None' = None,
|
|
307
|
+
log_type: 'str' = "stdout", size: 'int' = 1048576,
|
|
308
|
+
session_context: 'dict[str, Any] | None' = None,
|
|
309
|
+
) -> 'dict[str, Any]':
|
|
310
|
+
"""Read task diagnostics without hiding service errors or raw metrics.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
job_id: Instance ID resolved by the application.
|
|
314
|
+
section: task-detail, task-summary, workers, or worker-log.
|
|
315
|
+
project: Project owning the instance.
|
|
316
|
+
task_name: Explicit task name, or SDK single-task selection.
|
|
317
|
+
log_id: Worker log ID returned by worker discovery.
|
|
318
|
+
log_type: Supported PyODPS worker log type.
|
|
319
|
+
size: Positive requested log size in bytes.
|
|
320
|
+
session_context: Saved SQLRT routing context.
|
|
321
|
+
"""
|
|
322
|
+
if section not in {"task-detail", "task-summary", "workers", "worker-log"}:
|
|
323
|
+
raise ValidationError("Unknown job inspection section.")
|
|
324
|
+
if section == "worker-log":
|
|
325
|
+
from odps.models.worker import LOG_TYPES_MAPPING
|
|
326
|
+
|
|
327
|
+
if not log_id or not log_id.strip():
|
|
328
|
+
raise ValidationError("A non-empty worker log ID is required.")
|
|
329
|
+
if log_type not in LOG_TYPES_MAPPING or not isinstance(size, int) or size <= 0:
|
|
330
|
+
raise ValidationError("Choose a supported log type and a positive log size.")
|
|
331
|
+
instance = self._get_instance(job_id, project=project, session_context=session_context)
|
|
332
|
+
try:
|
|
333
|
+
if section == "worker-log":
|
|
334
|
+
return {"log_id": log_id, "log_type": log_type, "requested_size": size,
|
|
335
|
+
"content": instance.get_worker_log(log_id, log_type, size=size)}
|
|
336
|
+
if section == "task-summary":
|
|
337
|
+
if self._raw_attr(instance, "_subquery_id") is not None:
|
|
338
|
+
from ..exceptions import FeatureUnavailableError
|
|
339
|
+
|
|
340
|
+
raise FeatureUnavailableError(
|
|
341
|
+
"Task summaries are not scoped to SQLRT subqueries by PyODPS.",
|
|
342
|
+
suggestion="Use `job task-detail` for subquery-scoped metrics.",
|
|
343
|
+
)
|
|
344
|
+
summary = instance.get_task_summary(task_name)
|
|
345
|
+
return {"task_name": task_name, "available": summary is not None,
|
|
346
|
+
"summary": dict(summary) if summary is not None else None,
|
|
347
|
+
"summary_text": getattr(summary, "summary_text", None)}
|
|
348
|
+
detail = instance.get_task_detail2(task_name)
|
|
349
|
+
if section == "task-detail":
|
|
350
|
+
return {"task_name": task_name, "detail": detail}
|
|
351
|
+
if not isinstance(detail, (dict, list)):
|
|
352
|
+
from ..exceptions import FeatureUnavailableError
|
|
353
|
+
|
|
354
|
+
raise FeatureUnavailableError(
|
|
355
|
+
"Task detail is not structured JSON; worker discovery is unavailable.",
|
|
356
|
+
suggestion="Read `job task-detail` to inspect the returned detail.",
|
|
357
|
+
)
|
|
358
|
+
workers = instance.get_task_workers(task_name, json_obj=detail)
|
|
359
|
+
fields = ("id", "log_id", "type", "status", "start_time", "end_time",
|
|
360
|
+
"input_bytes", "input_records", "output_bytes", "output_records")
|
|
361
|
+
return {"task_name": task_name, "workers": [
|
|
362
|
+
{field: getattr(worker, field, None) for field in fields} for worker in workers
|
|
363
|
+
]}
|
|
364
|
+
except MaxCError:
|
|
365
|
+
raise
|
|
366
|
+
except Exception as exc:
|
|
367
|
+
raise translate_odps_error(exc, context="job") from exc
|
|
368
|
+
|
|
304
369
|
def list_jobs(
|
|
305
370
|
self, *, project: 'str | None' = None, limit: 'int' = 20
|
|
306
371
|
) -> 'tuple[list[JobInfo], bool]':
|
|
@@ -19,7 +19,7 @@ from . import agent_platforms
|
|
|
19
19
|
from ._samples import SAMPLES
|
|
20
20
|
from .exceptions import ErrorPayload, MaxCError, ValidationError
|
|
21
21
|
from .help_format import AliyunRawTextFormatter, AliyunStyleFormatter
|
|
22
|
-
from .models import AgentHints, Envelope, action, suggested_action_is_safe
|
|
22
|
+
from .models import AgentHints, Envelope, SuggestedAction, action, suggested_action_is_safe
|
|
23
23
|
from .output import emit_json, emit_ndjson, render_error, render_key_values, render_table
|
|
24
24
|
from .utils import current_cli_entry_point, extract_table_names, now_utc_iso, read_sql_input
|
|
25
25
|
|
|
@@ -381,6 +381,24 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
381
381
|
job_diagnose.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
382
382
|
job_diagnose.set_defaults(handler=_handle_job_diagnose)
|
|
383
383
|
|
|
384
|
+
for name, description in (
|
|
385
|
+
("task-detail", "Read raw task detail v2, including operator metrics"),
|
|
386
|
+
("task-summary", "Read the service task summary"),
|
|
387
|
+
("workers", "List task workers and their log IDs"),
|
|
388
|
+
("worker-log", "Read one worker log with a bounded requested size"),
|
|
389
|
+
):
|
|
390
|
+
inspection = _make_parser(job_subparsers, name, f"job.{name}", help=description)
|
|
391
|
+
inspection.add_argument("job_id", help="Job or instance ID")
|
|
392
|
+
inspection.add_argument("--project", help="Project owning the job (uses stored context when omitted)")
|
|
393
|
+
inspection.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
394
|
+
if name == "worker-log":
|
|
395
|
+
inspection.add_argument("log_id", help="Worker log_id returned by job workers")
|
|
396
|
+
inspection.add_argument("--log-type", choices=("stdout", "stderr", "waterfall_summary", "jstack", "pstack", "hs_err_log", "coreinfo"), default="stdout")
|
|
397
|
+
inspection.add_argument("--size", type=positive_int, default=1048576, help="Requested log size in bytes (default: 1048576; must be positive)")
|
|
398
|
+
else:
|
|
399
|
+
inspection.add_argument("--task-name", help="Task name; omit only for a single-task instance")
|
|
400
|
+
inspection.set_defaults(handler=_handle_job_inspect, inspection_section=name)
|
|
401
|
+
|
|
384
402
|
job_result = _make_parser(job_subparsers, "result", "job.result", help="Fetch job results")
|
|
385
403
|
job_result.add_argument("job_id", help="Job ID returned by submit")
|
|
386
404
|
job_result.add_argument("--project", help="Project that owns the job (uses stored submission context when omitted)")
|
|
@@ -768,7 +786,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
768
786
|
"--project",
|
|
769
787
|
help="Target MaxCompute project (omit to pop an interactive picker over the catalog)",
|
|
770
788
|
)
|
|
771
|
-
auth_login.add_argument("--endpoint", help="MaxCompute endpoint URL")
|
|
789
|
+
auth_login.add_argument("--endpoint", "--odps-endpoint", dest="endpoint", help="MaxCompute endpoint URL (defaults from project/profile region)")
|
|
790
|
+
auth_login.add_argument(
|
|
791
|
+
"--reuse-auth", action="store_true",
|
|
792
|
+
help="Select project/endpoint using existing credentials; save connection settings only",
|
|
793
|
+
)
|
|
772
794
|
auth_login.add_argument("--region", dest="region_name", help="MaxCompute region name")
|
|
773
795
|
auth_login.add_argument("--tunnel-endpoint", help="Tunnel endpoint URL for data transfer")
|
|
774
796
|
auth_login.add_argument(
|
|
@@ -1610,11 +1632,14 @@ def run(
|
|
|
1610
1632
|
app: MaxCApp | None = None
|
|
1611
1633
|
try:
|
|
1612
1634
|
_validate_output_request(args, command_name)
|
|
1613
|
-
|
|
1614
|
-
cwd
|
|
1615
|
-
config_path
|
|
1616
|
-
load_backend
|
|
1617
|
-
|
|
1635
|
+
app_kwargs = {
|
|
1636
|
+
"cwd": working_dir,
|
|
1637
|
+
"config_path": config_path,
|
|
1638
|
+
"load_backend": _should_load_backend(command_name),
|
|
1639
|
+
}
|
|
1640
|
+
if app_kwargs["load_backend"] and getattr(args, "project", None):
|
|
1641
|
+
app_kwargs["project_override"] = args.project
|
|
1642
|
+
app = app_type(**app_kwargs)
|
|
1618
1643
|
args.handler(app, args, stdout)
|
|
1619
1644
|
return getattr(args, "_envelope_exit_code", 0)
|
|
1620
1645
|
except MaxCError as exc:
|
|
@@ -1684,6 +1709,40 @@ def run(
|
|
|
1684
1709
|
),
|
|
1685
1710
|
}
|
|
1686
1711
|
_hints = _error_hints.get(exc.error_code)
|
|
1712
|
+
missing_fields = (exc.context or {}).get("missing_fields")
|
|
1713
|
+
if (exc.error_code == "VALIDATION_ERROR" and missing_fields
|
|
1714
|
+
and set(missing_fields) <= {"project", "endpoint"}):
|
|
1715
|
+
prefix = current_cli_entry_point()
|
|
1716
|
+
if requested_config_path is not None:
|
|
1717
|
+
prefix += " --config " + shlex.quote(str(requested_config_path))
|
|
1718
|
+
if getattr(args, "user_agent", None):
|
|
1719
|
+
prefix += " --user-agent " + shlex.quote(args.user_agent)
|
|
1720
|
+
exc.recoverable = True
|
|
1721
|
+
connection_action = SuggestedAction(
|
|
1722
|
+
id="auth.login", title="Configure connection using existing credentials",
|
|
1723
|
+
command=prefix + " auth login --reuse-auth --json",
|
|
1724
|
+
executable=True, effect="local_write", confirmation_required=True,
|
|
1725
|
+
agent_allowed=True,
|
|
1726
|
+
)
|
|
1727
|
+
actions = []
|
|
1728
|
+
if "project" in missing_fields and hasattr(args, "project"):
|
|
1729
|
+
retry_tokens = [*current_cli_entry_point().split(), *argv_list,
|
|
1730
|
+
"--project", "__MAXC_PROJECT__"]
|
|
1731
|
+
retry_command = " ".join(shlex.quote(token) for token in retry_tokens).replace("__MAXC_PROJECT__", "<project>")
|
|
1732
|
+
original_action = action(command_name)
|
|
1733
|
+
forced_write = bool(getattr(args, "force", False))
|
|
1734
|
+
actions.append(SuggestedAction(
|
|
1735
|
+
id=command_name, title="Specify the target project",
|
|
1736
|
+
command=retry_command, executable=False,
|
|
1737
|
+
effect="remote_write" if forced_write else original_action.effect,
|
|
1738
|
+
confirmation_required=forced_write or original_action.confirmation_required,
|
|
1739
|
+
agent_allowed=original_action.agent_allowed,
|
|
1740
|
+
placeholders={"project": "<project>"},
|
|
1741
|
+
))
|
|
1742
|
+
exc.suggestion = "Specify the target project: " + retry_command
|
|
1743
|
+
if "endpoint" in missing_fields:
|
|
1744
|
+
exc.suggestion += ". Configure the missing endpoint before retrying."
|
|
1745
|
+
_hints = AgentHints(actions=actions + [connection_action])
|
|
1687
1746
|
# Build schema context for SQL errors to enable agent self-correction
|
|
1688
1747
|
if app is not None and exc.error_code in (
|
|
1689
1748
|
"SQL_ERROR", "NOT_FOUND", "SCHEMA_NOT_FOUND", "TABLE_NOT_FOUND", "COLUMN_NOT_FOUND",
|
|
@@ -1707,6 +1766,10 @@ def run(
|
|
|
1707
1766
|
error=exc.to_payload(),
|
|
1708
1767
|
agent_hints=_hints,
|
|
1709
1768
|
)
|
|
1769
|
+
if missing_fields and set(missing_fields) <= {"project", "endpoint"} and _hints:
|
|
1770
|
+
payload.error.recovery_steps = [
|
|
1771
|
+
item.command for item in _hints.actions
|
|
1772
|
+
]
|
|
1710
1773
|
if _is_job_command(args):
|
|
1711
1774
|
_prepare_job_failure_envelope(payload, args)
|
|
1712
1775
|
if _is_job_wait_stream(args):
|
|
@@ -1992,6 +2055,15 @@ def _prepare_job_failure_envelope(
|
|
|
1992
2055
|
envelope.agent_hints = AgentHints(actions=actions, warnings=warnings)
|
|
1993
2056
|
|
|
1994
2057
|
|
|
2058
|
+
def _handle_job_inspect(app: MaxCApp, args: argparse.Namespace, stdout: TextIO) -> None:
|
|
2059
|
+
envelope = app.job_inspect(
|
|
2060
|
+
args.job_id, section=args.inspection_section, project=args.project,
|
|
2061
|
+
task_name=getattr(args, "task_name", None), log_id=getattr(args, "log_id", None),
|
|
2062
|
+
log_type=getattr(args, "log_type", "stdout"), size=getattr(args, "size", 1048576),
|
|
2063
|
+
)
|
|
2064
|
+
_emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
|
|
2065
|
+
|
|
2066
|
+
|
|
1995
2067
|
def _handle_job_diagnose(app: MaxCApp, args: argparse.Namespace, stdout: TextIO) -> None:
|
|
1996
2068
|
envelope = app.job_diagnose(args.job_id, project=args.project)
|
|
1997
2069
|
_emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
|
|
@@ -2413,6 +2485,14 @@ def _handle_auth_login(app: MaxCApp, args: argparse.Namespace, stdout: TextIO) -
|
|
|
2413
2485
|
args.security_token,
|
|
2414
2486
|
)
|
|
2415
2487
|
)
|
|
2488
|
+
if args.reuse_auth and (
|
|
2489
|
+
args.oauth or args.from_env or explicit_credentials
|
|
2490
|
+
or args.login_continuation or args.oauth_continuation
|
|
2491
|
+
):
|
|
2492
|
+
raise ValidationError(
|
|
2493
|
+
"`--reuse-auth` cannot be combined with a new identity or auth continuation.",
|
|
2494
|
+
suggestion="Use --reuse-auth alone to configure the existing identity's connection.",
|
|
2495
|
+
)
|
|
2416
2496
|
if args.oauth:
|
|
2417
2497
|
if args.from_env or explicit_credentials:
|
|
2418
2498
|
raise ValidationError(
|
|
@@ -2479,6 +2559,8 @@ def _handle_auth_login(app: MaxCApp, args: argparse.Namespace, stdout: TextIO) -
|
|
|
2479
2559
|
region_name=args.region_name,
|
|
2480
2560
|
tunnel_endpoint=args.tunnel_endpoint,
|
|
2481
2561
|
from_env=args.from_env,
|
|
2562
|
+
reuse_auth=args.reuse_auth,
|
|
2563
|
+
allow_interactive_picker=not args.reuse_auth or not _is_json_mode(args),
|
|
2482
2564
|
no_validate=args.no_validate,
|
|
2483
2565
|
target_config_path=args.requested_config_path,
|
|
2484
2566
|
catalog_endpoint=args.catalog_endpoint,
|
|
@@ -2588,6 +2670,10 @@ _MANIFEST_CONDITIONAL_NETWORK_COMMANDS = frozenset({
|
|
|
2588
2670
|
_MANIFEST_JOB_FOLLOWUP_COMMANDS = frozenset({
|
|
2589
2671
|
"job.cancel",
|
|
2590
2672
|
"job.diagnose",
|
|
2673
|
+
"job.task-detail",
|
|
2674
|
+
"job.task-summary",
|
|
2675
|
+
"job.workers",
|
|
2676
|
+
"job.worker-log",
|
|
2591
2677
|
"job.result",
|
|
2592
2678
|
"job.status",
|
|
2593
2679
|
"job.wait",
|
|
@@ -2690,6 +2776,14 @@ def _manifest_requirements(command: str) -> dict[str, Any]:
|
|
|
2690
2776
|
"mode": "required",
|
|
2691
2777
|
"reason": "A new OAuth authorization and STS exchange use network APIs.",
|
|
2692
2778
|
},
|
|
2779
|
+
{
|
|
2780
|
+
"when": {"all": [
|
|
2781
|
+
_manifest_condition("reuse_auth", equals=True),
|
|
2782
|
+
{"runtime": "active_provider_is_oauth_and_cached_sts_expiring"},
|
|
2783
|
+
]},
|
|
2784
|
+
"mode": "required",
|
|
2785
|
+
"reason": "Reusing saved OAuth may refresh temporary credentials without browser authorization.",
|
|
2786
|
+
},
|
|
2693
2787
|
{
|
|
2694
2788
|
"when": _manifest_condition("no_validate", equals=False),
|
|
2695
2789
|
"mode": "required",
|
|
@@ -2937,12 +3031,16 @@ def _manifest_effects(command: str) -> list[dict[str, Any]]:
|
|
|
2937
3031
|
"create_or_replace",
|
|
2938
3032
|
"auth_config",
|
|
2939
3033
|
when={"runtime": "login_succeeds"},
|
|
3034
|
+
note="--reuse-auth preserves the existing identity and writes connection fields only.",
|
|
2940
3035
|
),
|
|
2941
3036
|
_manifest_effect(
|
|
2942
3037
|
"local",
|
|
2943
3038
|
"create",
|
|
2944
3039
|
"owner_only_access_key_continuation",
|
|
2945
|
-
when={"
|
|
3040
|
+
when={"all": [
|
|
3041
|
+
{"runtime": "access_key_project_selection_pending"},
|
|
3042
|
+
_manifest_condition("reuse_auth", equals=False),
|
|
3043
|
+
]},
|
|
2946
3044
|
),
|
|
2947
3045
|
_manifest_effect(
|
|
2948
3046
|
"local",
|
|
@@ -381,6 +381,8 @@ class MaxCConfig:
|
|
|
381
381
|
sources: 'list[Path]'
|
|
382
382
|
mcqa: 'McqaConfig' = field(default_factory=McqaConfig)
|
|
383
383
|
mcp: 'McpConfig' = field(default_factory=McpConfig)
|
|
384
|
+
# Only set in memory for this invocation, never loaded or saved from YAML.
|
|
385
|
+
command_project: 'str | None' = None
|
|
384
386
|
|
|
385
387
|
|
|
386
388
|
def _optional_string(value: 'Any') -> 'str | None':
|