maxc-cli 0.8.0__tar.gz → 0.8.2__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.8.0 → maxc_cli-0.8.2}/PKG-INFO +1 -1
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/__init__.py +1 -1
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/_samples.py +4 -3
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/app.py +181 -47
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/auth_providers.py +19 -3
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/backend/job.py +4 -2
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/backend/odps.py +18 -4
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/backend/query.py +0 -1
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/cli.py +86 -10
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/config.py +2 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/exceptions.py +1 -1
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/helpers.py +18 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/models.py +6 -1
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/oauth.py +12 -5
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/output.py +21 -4
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/skills/references/bootstrap-auth.md +22 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/utils.py +42 -7
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli.egg-info/PKG-INFO +1 -1
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli.egg-info/SOURCES.txt +1 -0
- maxc_cli-0.8.2/tests/test_connection_bootstrap.py +279 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_job_improvements.py +52 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_oauth.py +40 -4
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_output_action_safety.py +25 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_output_format_contract.py +15 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_query_auto_promote.py +96 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_query_result_csv_fallback.py +26 -2
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/MANIFEST.in +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/README.md +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/pyproject.toml +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/scripts/pyinstaller_entry.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/scripts/regression_test.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/setup.cfg +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/setup.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/__main__.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/agent_platforms.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/audit.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/auth_continuation.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/backend/__init__.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/backend/auth.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/backend/catalog.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/backend/data.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/backend/mcp.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/backend/meta.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/backend/semantic.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/cache.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/catalog_bootstrap.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/enterprise_tls.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/help_format.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/job_ids.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/masking.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/mcp_serve.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/odps_runtime.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/proxy_auth.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/semantic.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/semantic_management.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/setting_parser.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/skills/SKILL.md +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/skills/agents/openai.yaml +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/skills/references/bootstrap-flow.md +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/skills/references/command-patterns.md +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/skills/references/json-output-format.md +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/skills/references/maxcompute-select-guide.md +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/skills/references/maxcompute-sql-notes.md +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/skills/references/partition-guide.md +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/skills/references/red-lines.md +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/skills/references/semantic-packages.md +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/skills/references/setup-install.md +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/skills/references/sql-common-errors.md +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/skills/references/sql-query-patterns.md +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/skills/references/text2sql-principles.md +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/state_permissions.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/store.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli.egg-info/dependency_links.txt +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli.egg-info/entry_points.txt +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli.egg-info/requires.txt +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli.egg-info/top_level.txt +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_agent_hints_and_cli.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_agent_platforms.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_agent_skill_commands.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_agent_skill_commands_context.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_ai_native_contract_regressions.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_auth_logout.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_backend_auth.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_backend_data.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_backend_data_serialization.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_backend_mcp.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_backend_meta.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_build_release_archive_compat.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_build_release_script.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_cache.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_catalog.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_catalog_bootstrap.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_cli_arg_validation.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_cli_mock.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_cli_query_parse_and_sanitize.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_compat.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_config_atomic_write.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_e2e_smoke.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_effective_hints_contract.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_enterprise_tls.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_envelope_shape.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_error_self_correction.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_error_translation.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_exit_codes.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_external_auth.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_flag_hoist.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_help_format.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_help_version_e2e.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_helpers.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_helpers_csv.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_installer_contracts.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_integration.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_integration_real.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_job_inspection.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_job_store_durability.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_kb_commands.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_manifest_runtime_contract.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_masking.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_mcp_serve.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_meta_schema_and_partition_cols.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_odps_runtime.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_packaging_metadata.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_phase1_improvements.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_proxy_auth.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_pyinstaller_bundle.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_python39_compat.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_semantic_management.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_semantic_scope.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_semantic_transport.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_setting_parser.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_skill_cli_consistency.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_skill_eval.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_skill_renderer.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_startup_imports.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_state_permissions.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_state_portability.py +0 -0
|
@@ -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",
|
|
@@ -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
|
|
@@ -859,13 +866,22 @@ class MaxCApp:
|
|
|
859
866
|
session["job_id"],
|
|
860
867
|
project=session.get("project") or None,
|
|
861
868
|
)
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
+
try:
|
|
870
|
+
result = self.backend.fetch_job_result(
|
|
871
|
+
resolved.instance_id,
|
|
872
|
+
project=resolved.project,
|
|
873
|
+
max_rows=max_rows,
|
|
874
|
+
offset=offset,
|
|
875
|
+
session_context=resolved.session_context,
|
|
876
|
+
)
|
|
877
|
+
except Exception as exc:
|
|
878
|
+
error = exc if isinstance(exc, MaxCError) else translate_odps_error(exc)
|
|
879
|
+
envelope = self._job_result_fetch_failure(
|
|
880
|
+
command, resolved, None, error.to_payload(),
|
|
881
|
+
max_rows=max_rows, cursor=cursor,
|
|
882
|
+
)
|
|
883
|
+
self.log(command, envelope.status, envelope.metadata)
|
|
884
|
+
return envelope
|
|
869
885
|
envelope = self._build_query_envelope(
|
|
870
886
|
command=command,
|
|
871
887
|
result=result,
|
|
@@ -1091,25 +1107,13 @@ class MaxCApp:
|
|
|
1091
1107
|
except Exception as exc:
|
|
1092
1108
|
fetch_error = translate_odps_error(exc).to_payload()
|
|
1093
1109
|
if fetch_error is not None:
|
|
1094
|
-
envelope =
|
|
1095
|
-
command=
|
|
1096
|
-
status="failure",
|
|
1097
|
-
data=None,
|
|
1098
|
-
error=fetch_error,
|
|
1110
|
+
envelope = self._job_result_fetch_failure(
|
|
1111
|
+
command, resolved, job_info, fetch_error, max_rows=max_rows,
|
|
1099
1112
|
metadata={
|
|
1100
|
-
"job_id": resolved.external_job_id,
|
|
1101
|
-
"project": resolved.project,
|
|
1102
|
-
"logview": job_info.logview,
|
|
1103
1113
|
"sql_executed": sql,
|
|
1104
1114
|
**execution_metadata,
|
|
1105
1115
|
},
|
|
1106
|
-
|
|
1107
|
-
actions=[
|
|
1108
|
-
action("job.result", data={"job_id": resolved.external_job_id}, metadata={"job_id": resolved.external_job_id, "project": resolved.project, "sql_executed": sql}),
|
|
1109
|
-
action("job.status", data={"job_id": resolved.external_job_id}, metadata={"job_id": resolved.external_job_id, "project": resolved.project}),
|
|
1110
|
-
],
|
|
1111
|
-
warnings=common_warnings,
|
|
1112
|
-
),
|
|
1116
|
+
warnings=common_warnings,
|
|
1113
1117
|
)
|
|
1114
1118
|
if idempotency_key:
|
|
1115
1119
|
envelope.metadata["idempotency_key"] = idempotency_key
|
|
@@ -1451,12 +1455,20 @@ class MaxCApp:
|
|
|
1451
1455
|
]
|
|
1452
1456
|
self.log("job.wait", envelope.status, envelope.metadata)
|
|
1453
1457
|
return envelope, events
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1458
|
+
try:
|
|
1459
|
+
result = self.backend.fetch_job_result(
|
|
1460
|
+
resolved.instance_id,
|
|
1461
|
+
project=resolved.project,
|
|
1462
|
+
max_rows=100,
|
|
1463
|
+
session_context=resolved.session_context,
|
|
1464
|
+
)
|
|
1465
|
+
except Exception as exc:
|
|
1466
|
+
error = exc if isinstance(exc, MaxCError) else translate_odps_error(exc)
|
|
1467
|
+
envelope = self._job_result_fetch_failure(
|
|
1468
|
+
"job.wait", resolved, after, error.to_payload(), max_rows=100,
|
|
1469
|
+
)
|
|
1470
|
+
self.log("job.wait", envelope.status, envelope.metadata)
|
|
1471
|
+
return envelope, []
|
|
1460
1472
|
envelope = self._build_query_envelope(
|
|
1461
1473
|
command="job.wait",
|
|
1462
1474
|
result=result,
|
|
@@ -1554,13 +1566,22 @@ class MaxCApp:
|
|
|
1554
1566
|
)
|
|
1555
1567
|
self.log("job.result", envelope.status, envelope.metadata)
|
|
1556
1568
|
return envelope
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1569
|
+
try:
|
|
1570
|
+
result = self.backend.fetch_job_result(
|
|
1571
|
+
resolved.instance_id,
|
|
1572
|
+
project=resolved.project,
|
|
1573
|
+
max_rows=max_rows,
|
|
1574
|
+
offset=offset,
|
|
1575
|
+
session_context=resolved.session_context,
|
|
1576
|
+
)
|
|
1577
|
+
except Exception as exc:
|
|
1578
|
+
error = exc if isinstance(exc, MaxCError) else translate_odps_error(exc)
|
|
1579
|
+
envelope = self._job_result_fetch_failure(
|
|
1580
|
+
"job.result", resolved, info, error.to_payload(),
|
|
1581
|
+
max_rows=max_rows, cursor=cursor,
|
|
1582
|
+
)
|
|
1583
|
+
self.log("job.result", envelope.status, envelope.metadata)
|
|
1584
|
+
return envelope
|
|
1564
1585
|
envelope = self._build_query_envelope(
|
|
1565
1586
|
command="job.result",
|
|
1566
1587
|
result=result,
|
|
@@ -1662,6 +1683,48 @@ class MaxCApp:
|
|
|
1662
1683
|
self.log("job.result", envelope.status, envelope.metadata)
|
|
1663
1684
|
return envelope
|
|
1664
1685
|
|
|
1686
|
+
def _job_result_fetch_failure(
|
|
1687
|
+
self,
|
|
1688
|
+
command: 'str',
|
|
1689
|
+
resolved: '_ResolvedExternalJobId',
|
|
1690
|
+
info: 'JobInfo | None',
|
|
1691
|
+
error: 'ErrorPayload',
|
|
1692
|
+
*,
|
|
1693
|
+
max_rows: 'int',
|
|
1694
|
+
cursor: 'str | None' = None,
|
|
1695
|
+
metadata: 'dict[str, Any] | None' = None,
|
|
1696
|
+
warnings: 'list[str] | None' = None,
|
|
1697
|
+
) -> 'Envelope':
|
|
1698
|
+
"""Keep the existing job and page observable when reading fails."""
|
|
1699
|
+
scope = {
|
|
1700
|
+
"job_id": resolved.external_job_id,
|
|
1701
|
+
"project": resolved.project,
|
|
1702
|
+
"logview": info.logview if info else error.logview,
|
|
1703
|
+
**({
|
|
1704
|
+
"job_status": info.status,
|
|
1705
|
+
"submitted_at": info.submitted_at,
|
|
1706
|
+
"completed_at": info.completed_at,
|
|
1707
|
+
} if info else {}),
|
|
1708
|
+
**(metadata or {}),
|
|
1709
|
+
}
|
|
1710
|
+
retry = action("job.result", data={"max_rows": max_rows, "cursor": cursor}, metadata=scope)
|
|
1711
|
+
error.instance_id = resolved.external_job_id
|
|
1712
|
+
error.logview = scope["logview"]
|
|
1713
|
+
error.recovery_steps = [
|
|
1714
|
+
"Retry reading the existing job result: " + retry.command,
|
|
1715
|
+
"Do not resubmit the SQL to recover a result-reader failure.",
|
|
1716
|
+
]
|
|
1717
|
+
return Envelope(
|
|
1718
|
+
command=command,
|
|
1719
|
+
status="failure",
|
|
1720
|
+
error=error,
|
|
1721
|
+
metadata=scope,
|
|
1722
|
+
agent_hints=AgentHints(
|
|
1723
|
+
actions=[retry, action("job.diagnose", metadata=scope)],
|
|
1724
|
+
warnings=warnings or [],
|
|
1725
|
+
),
|
|
1726
|
+
)
|
|
1727
|
+
|
|
1665
1728
|
def _bound_job_result_cursor(
|
|
1666
1729
|
self,
|
|
1667
1730
|
cursor: 'str | None',
|
|
@@ -4708,6 +4771,7 @@ class MaxCApp:
|
|
|
4708
4771
|
projects: 'list[dict[str, Any]]',
|
|
4709
4772
|
*,
|
|
4710
4773
|
title_prefix: 'str',
|
|
4774
|
+
endpoint_flag: 'str' = "--endpoint",
|
|
4711
4775
|
) -> 'list[SuggestedAction]':
|
|
4712
4776
|
actions: list[SuggestedAction] = []
|
|
4713
4777
|
for project_data in projects:
|
|
@@ -4717,7 +4781,7 @@ class MaxCApp:
|
|
|
4717
4781
|
tokens = [*base_tokens, "--project", project_id]
|
|
4718
4782
|
endpoint_value = project_data.get("endpoint")
|
|
4719
4783
|
endpoint_placeholder = endpoint_value is None
|
|
4720
|
-
tokens.extend([
|
|
4784
|
+
tokens.extend([endpoint_flag, str(endpoint_value or "__MAXC_ENDPOINT__")])
|
|
4721
4785
|
if project_data.get("region"):
|
|
4722
4786
|
tokens.extend(["--region", str(project_data["region"])])
|
|
4723
4787
|
if project_data.get("tunnel_endpoint"):
|
|
@@ -4755,6 +4819,8 @@ class MaxCApp:
|
|
|
4755
4819
|
region_name: 'str | None' = None,
|
|
4756
4820
|
tunnel_endpoint: 'str | None' = None,
|
|
4757
4821
|
from_env: 'bool' = False,
|
|
4822
|
+
reuse_auth: 'bool' = False,
|
|
4823
|
+
allow_interactive_picker: 'bool' = True,
|
|
4758
4824
|
no_validate: 'bool' = False,
|
|
4759
4825
|
target_config_path: 'Path | None' = None,
|
|
4760
4826
|
catalog_endpoint: 'str | None' = None,
|
|
@@ -4768,8 +4834,39 @@ class MaxCApp:
|
|
|
4768
4834
|
existing_auth = AuthConfig.from_mapping(existing_payload.get("auth", {}) or {})
|
|
4769
4835
|
env_settings = load_odps_env()
|
|
4770
4836
|
|
|
4771
|
-
#
|
|
4772
|
-
|
|
4837
|
+
# Connection setup reuses the effective identity, including the
|
|
4838
|
+
# wrapper's refreshable profile, without importing its temporary keys.
|
|
4839
|
+
if reuse_auth:
|
|
4840
|
+
if any((access_id, secret_access_key, security_token, continuation_id)) or from_env or _oauth_config:
|
|
4841
|
+
raise ValidationError("--reuse-auth cannot replace credentials or resume a login.")
|
|
4842
|
+
effective, _, _ = resolve_odps_settings(self.config)
|
|
4843
|
+
provider = infer_auth_provider(self.config, effective)
|
|
4844
|
+
resolved_access_id = effective.get("access_id")
|
|
4845
|
+
resolved_secret = effective.get("secret_access_key")
|
|
4846
|
+
resolved_token = effective.get("security_token")
|
|
4847
|
+
if provider == "oauth":
|
|
4848
|
+
from .auth_providers import _oauth_persist_path
|
|
4849
|
+
from .oauth import ensure_oauth_sts
|
|
4850
|
+
|
|
4851
|
+
sts = ensure_oauth_sts(self.config.auth, config_path=_oauth_persist_path(self.config))
|
|
4852
|
+
resolved_access_id = sts.access_key_id
|
|
4853
|
+
resolved_secret = sts.access_key_secret
|
|
4854
|
+
resolved_token = sts.security_token
|
|
4855
|
+
if provider == "sts_token" and not resolved_token:
|
|
4856
|
+
raise ValidationError(
|
|
4857
|
+
"The existing STS identity is missing its security token.",
|
|
4858
|
+
suggestion="Restore the existing STS profile before configuring the connection.",
|
|
4859
|
+
)
|
|
4860
|
+
if not resolved_access_id or not resolved_secret:
|
|
4861
|
+
raise ValidationError(
|
|
4862
|
+
"No reusable AccessKey/STS/OAuth credentials are available.",
|
|
4863
|
+
suggestion="Configure the intended identity before selecting a project.",
|
|
4864
|
+
)
|
|
4865
|
+
# Preserve the active provider's suppression of unrelated shell
|
|
4866
|
+
# values. Routing defaults below come from its effective settings.
|
|
4867
|
+
env_settings = effective
|
|
4868
|
+
existing_auth = AuthConfig.from_mapping(self.config.auth.to_mapping())
|
|
4869
|
+
elif continuation_id:
|
|
4773
4870
|
if any((access_id, secret_access_key, security_token)) or from_env:
|
|
4774
4871
|
raise ValidationError(
|
|
4775
4872
|
"Do not combine an auth continuation with credential flags or --from-env.",
|
|
@@ -4848,6 +4945,7 @@ class MaxCApp:
|
|
|
4848
4945
|
env_settings=env_settings,
|
|
4849
4946
|
existing_auth=existing_auth,
|
|
4850
4947
|
reselect=reselect,
|
|
4948
|
+
allow_interactive=allow_interactive_picker,
|
|
4851
4949
|
)
|
|
4852
4950
|
)
|
|
4853
4951
|
except ProjectPickerPending as exc:
|
|
@@ -4895,6 +4993,18 @@ class MaxCApp:
|
|
|
4895
4993
|
warnings=[],
|
|
4896
4994
|
),
|
|
4897
4995
|
)
|
|
4996
|
+
if reuse_auth:
|
|
4997
|
+
base_tokens = self._auth_action_prefix(target_path)
|
|
4998
|
+
base_tokens.extend(["auth", "login", "--reuse-auth"])
|
|
4999
|
+
if catalog_endpoint:
|
|
5000
|
+
base_tokens.extend(["--catalog-endpoint", catalog_endpoint])
|
|
5001
|
+
if no_validate:
|
|
5002
|
+
base_tokens.append("--no-validate")
|
|
5003
|
+
pending_envelope.agent_hints.actions = self._project_selection_actions(
|
|
5004
|
+
base_tokens, projects_data, title_prefix="Configure connection for",
|
|
5005
|
+
endpoint_flag="--odps-endpoint",
|
|
5006
|
+
)
|
|
5007
|
+
return pending_envelope
|
|
4898
5008
|
if _oauth_config is not None:
|
|
4899
5009
|
return pending_envelope
|
|
4900
5010
|
if continuation_id:
|
|
@@ -4934,6 +5044,15 @@ class MaxCApp:
|
|
|
4934
5044
|
pending_envelope.metadata["continuation_expires_at_unix"] = expires_at
|
|
4935
5045
|
return pending_envelope
|
|
4936
5046
|
|
|
5047
|
+
from .catalog_bootstrap import region_to_endpoint, region_to_tunnel_endpoint
|
|
5048
|
+
|
|
5049
|
+
fallback_region = derived_region or env_settings.get("region_name") or existing_auth.region_name
|
|
5050
|
+
derived_region = fallback_region
|
|
5051
|
+
if not derived_endpoint and not env_settings.get("endpoint") and not existing_auth.endpoint:
|
|
5052
|
+
derived_endpoint = region_to_endpoint(fallback_region)
|
|
5053
|
+
if not derived_tunnel and not env_settings.get("tunnel_endpoint") and not existing_auth.tunnel_endpoint:
|
|
5054
|
+
derived_tunnel = region_to_tunnel_endpoint(fallback_region)
|
|
5055
|
+
|
|
4937
5056
|
resolved_auth = AuthConfig(
|
|
4938
5057
|
access_id=resolved_access_id,
|
|
4939
5058
|
secret_access_key=resolved_secret,
|
|
@@ -4983,10 +5102,25 @@ class MaxCApp:
|
|
|
4983
5102
|
persisted_auth.provider = "oauth"
|
|
4984
5103
|
persisted_auth.oauth = _oauth_config
|
|
4985
5104
|
migrate_legacy_session_override(target_path)
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
5105
|
+
if reuse_auth:
|
|
5106
|
+
connection_fields = {
|
|
5107
|
+
name: getattr(resolved_auth, name)
|
|
5108
|
+
for name in ("project", "endpoint", "region_name", "tunnel_endpoint")
|
|
5109
|
+
if getattr(resolved_auth, name)
|
|
5110
|
+
}
|
|
5111
|
+
|
|
5112
|
+
def save_connection(payload: 'dict[str, Any]') -> 'None':
|
|
5113
|
+
auth_mapping = payload.setdefault("auth", {})
|
|
5114
|
+
if not isinstance(auth_mapping, dict):
|
|
5115
|
+
raise ValidationError("The auth configuration must be a mapping.")
|
|
5116
|
+
auth_mapping.update(connection_fields)
|
|
5117
|
+
payload["default_project"] = resolved_auth.project
|
|
5118
|
+
if resolved_auth.region_name:
|
|
5119
|
+
payload["default_region"] = resolved_auth.region_name
|
|
5120
|
+
|
|
5121
|
+
update_config_mapping(target_path, save_connection)
|
|
5122
|
+
else:
|
|
5123
|
+
persist_login_config(target_path, auth=persisted_auth)
|
|
4990
5124
|
|
|
4991
5125
|
continuation_cleanup_warning: str | None = None
|
|
4992
5126
|
if continuation_id:
|
|
@@ -5007,7 +5141,7 @@ class MaxCApp:
|
|
|
5007
5141
|
# Always remind callers that AK/SK is stored in plaintext YAML (chmod
|
|
5008
5142
|
# 0600) — flagged in CLAUDE.md as a known limitation. Skip for STS
|
|
5009
5143
|
# tokens since those are short-lived and self-expiring.
|
|
5010
|
-
if not resolved_auth.security_token:
|
|
5144
|
+
if not reuse_auth and not resolved_auth.security_token:
|
|
5011
5145
|
warnings.append(
|
|
5012
5146
|
f"AccessKey saved in plaintext at `{target_path}` (file mode 0600). "
|
|
5013
5147
|
f"For shared/CI environments prefer `auth login-external` with a credential helper, "
|
|
@@ -5017,7 +5151,7 @@ class MaxCApp:
|
|
|
5017
5151
|
warnings.append(
|
|
5018
5152
|
"Credentials were imported from environment variables (--from-env) and saved to config."
|
|
5019
5153
|
)
|
|
5020
|
-
elif any(
|
|
5154
|
+
elif not reuse_auth and any(
|
|
5021
5155
|
env_settings.get(name)
|
|
5022
5156
|
for name in ("access_id", "secret_access_key", "security_token", "endpoint", "region_name", "tunnel_endpoint")
|
|
5023
5157
|
):
|
|
@@ -5055,8 +5189,8 @@ class MaxCApp:
|
|
|
5055
5189
|
|
|
5056
5190
|
login_metadata = {
|
|
5057
5191
|
"config_path": str(target_path),
|
|
5058
|
-
"written_fields": sorted(persisted_auth.to_mapping().keys()),
|
|
5059
|
-
"auth_storage": "config_file",
|
|
5192
|
+
"written_fields": sorted(connection_fields if reuse_auth else persisted_auth.to_mapping().keys()),
|
|
5193
|
+
"auth_storage": "existing_identity" if reuse_auth else "config_file",
|
|
5060
5194
|
}
|
|
5061
5195
|
envelope = Envelope(
|
|
5062
5196
|
command="auth.login",
|
|
@@ -5750,7 +5884,7 @@ class MaxCApp:
|
|
|
5750
5884
|
from . import catalog_bootstrap as _catalog_bootstrap
|
|
5751
5885
|
|
|
5752
5886
|
# 2. Picker not viable (non-TTY or --no-picker).
|
|
5753
|
-
if inputs.no_picker or not sys.stdin.isatty():
|
|
5887
|
+
if inputs.no_picker or not inputs.allow_interactive or not sys.stdin.isatty():
|
|
5754
5888
|
# Non-TTY + picker not disabled: list projects for structured output
|
|
5755
5889
|
catalog_warning: str | None = None
|
|
5756
5890
|
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",
|
|
@@ -846,10 +846,12 @@ class JobMixin(QueryMixin):
|
|
|
846
846
|
return sql.rstrip(";") if sql else None
|
|
847
847
|
|
|
848
848
|
def _safe_logview(self, instance) -> 'str | None':
|
|
849
|
-
"""Get a LogView URL
|
|
849
|
+
"""Get a usable SDK LogView URL for the caller, without URL userinfo."""
|
|
850
850
|
from ..utils import sanitize_logview_url
|
|
851
851
|
|
|
852
852
|
try:
|
|
853
|
-
return sanitize_logview_url(
|
|
853
|
+
return sanitize_logview_url(
|
|
854
|
+
instance.get_logview_address(), include_access_token=True,
|
|
855
|
+
)
|
|
854
856
|
except Exception:
|
|
855
857
|
return None
|
|
@@ -152,7 +152,17 @@ class OdpsBackend(
|
|
|
152
152
|
# timeout), the column metadata is parsed lazily from the
|
|
153
153
|
# CSV header during the first __next__ call — so we can
|
|
154
154
|
# only inspect it after iteration begins.
|
|
155
|
-
|
|
155
|
+
iterator = iter(reader)
|
|
156
|
+
skipped = sum(1 for _ in islice(iterator, offset))
|
|
157
|
+
records = list(islice(iterator, max_rows))
|
|
158
|
+
count = getattr(reader, "count", None)
|
|
159
|
+
# CSV fallback readers expose no count. Count the
|
|
160
|
+
# remaining accessible records without retaining
|
|
161
|
+
# them so a page is not mistaken for the full result.
|
|
162
|
+
accessible_count = (
|
|
163
|
+
skipped + len(records) + sum(1 for _ in iterator)
|
|
164
|
+
if count is None else int(count)
|
|
165
|
+
)
|
|
156
166
|
|
|
157
167
|
reader_schema = getattr(reader, "schema", None)
|
|
158
168
|
if reader_schema is not None and hasattr(reader_schema, "columns"):
|
|
@@ -181,7 +191,7 @@ class OdpsBackend(
|
|
|
181
191
|
record_to_dict(column_names, record.values)
|
|
182
192
|
for record in records
|
|
183
193
|
]
|
|
184
|
-
total_rows =
|
|
194
|
+
total_rows = accessible_count
|
|
185
195
|
except Exception as reader_exc:
|
|
186
196
|
# Some SHOW results (e.g. multi-column SHOW INDEXES
|
|
187
197
|
# col_names) contain unquoted commas that make
|
|
@@ -197,7 +207,7 @@ class OdpsBackend(
|
|
|
197
207
|
rows = all_raw_rows[offset : offset + max_rows]
|
|
198
208
|
schema = [{"name": name, "type": "string", "comment": ""}
|
|
199
209
|
for name in _RAW_RESULT_COLUMNS]
|
|
200
|
-
total_rows = len(
|
|
210
|
+
total_rows = len(all_raw_rows)
|
|
201
211
|
|
|
202
212
|
for w in captured:
|
|
203
213
|
msg_text = str(w.message)
|
|
@@ -210,7 +220,10 @@ class OdpsBackend(
|
|
|
210
220
|
warnings.formatwarning(w.message, w.category, w.filename, w.lineno)
|
|
211
221
|
)
|
|
212
222
|
except Exception as exc:
|
|
213
|
-
|
|
223
|
+
error = translate_odps_error(exc)
|
|
224
|
+
error.instance_id = instance.id
|
|
225
|
+
error.logview = self._safe_logview(instance)
|
|
226
|
+
raise error from exc
|
|
214
227
|
|
|
215
228
|
if degraded_reason is not None:
|
|
216
229
|
fallback_warnings.append(
|
|
@@ -223,6 +236,7 @@ class OdpsBackend(
|
|
|
223
236
|
returned_rows = len(rows)
|
|
224
237
|
has_more = total_rows > (offset + returned_rows)
|
|
225
238
|
extra_metadata["current_offset"] = offset
|
|
239
|
+
extra_metadata["logview"] = self._safe_logview(instance)
|
|
226
240
|
if degraded_reason is not None:
|
|
227
241
|
extra_metadata["result_kind"] = "raw_task_result"
|
|
228
242
|
if resultless_statement:
|
|
@@ -506,7 +506,6 @@ class QueryMixin:
|
|
|
506
506
|
submitted_at=now_utc_iso(),
|
|
507
507
|
updated_at=now_utc_iso(),
|
|
508
508
|
logview=self._safe_logview(instance),
|
|
509
|
-
warnings=["The MaxCompute instance has been submitted; use job.status or job.wait to track it."],
|
|
510
509
|
session_task_name=session_task_name,
|
|
511
510
|
session_subquery_id=session_subquery_id,
|
|
512
511
|
session_project_name=getattr(getattr(instance, "project", None), "name", None),
|