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.
Files changed (136) hide show
  1. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/PKG-INFO +1 -1
  2. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/__init__.py +1 -1
  3. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/_samples.py +4 -3
  4. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/app.py +181 -47
  5. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/auth_providers.py +19 -3
  6. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/backend/job.py +4 -2
  7. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/backend/odps.py +18 -4
  8. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/backend/query.py +0 -1
  9. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/cli.py +86 -10
  10. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/config.py +2 -0
  11. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/exceptions.py +1 -1
  12. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/helpers.py +18 -0
  13. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/models.py +6 -1
  14. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/oauth.py +12 -5
  15. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/output.py +21 -4
  16. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/skills/references/bootstrap-auth.md +22 -0
  17. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/utils.py +42 -7
  18. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli.egg-info/PKG-INFO +1 -1
  19. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli.egg-info/SOURCES.txt +1 -0
  20. maxc_cli-0.8.2/tests/test_connection_bootstrap.py +279 -0
  21. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_job_improvements.py +52 -0
  22. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_oauth.py +40 -4
  23. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_output_action_safety.py +25 -0
  24. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_output_format_contract.py +15 -0
  25. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_query_auto_promote.py +96 -0
  26. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_query_result_csv_fallback.py +26 -2
  27. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/MANIFEST.in +0 -0
  28. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/README.md +0 -0
  29. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/pyproject.toml +0 -0
  30. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/scripts/pyinstaller_entry.py +0 -0
  31. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/scripts/regression_test.py +0 -0
  32. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/setup.cfg +0 -0
  33. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/setup.py +0 -0
  34. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/__main__.py +0 -0
  35. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/agent_platforms.py +0 -0
  36. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/audit.py +0 -0
  37. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/auth_continuation.py +0 -0
  38. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/backend/__init__.py +0 -0
  39. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/backend/auth.py +0 -0
  40. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/backend/catalog.py +0 -0
  41. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/backend/data.py +0 -0
  42. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/backend/mcp.py +0 -0
  43. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/backend/meta.py +0 -0
  44. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/backend/semantic.py +0 -0
  45. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/cache.py +0 -0
  46. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/catalog_bootstrap.py +0 -0
  47. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/enterprise_tls.py +0 -0
  48. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/help_format.py +0 -0
  49. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/job_ids.py +0 -0
  50. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/masking.py +0 -0
  51. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/mcp_serve.py +0 -0
  52. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/odps_runtime.py +0 -0
  53. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/proxy_auth.py +0 -0
  54. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/semantic.py +0 -0
  55. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/semantic_management.py +0 -0
  56. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/setting_parser.py +0 -0
  57. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/skills/SKILL.md +0 -0
  58. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/skills/agents/openai.yaml +0 -0
  59. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/skills/references/bootstrap-flow.md +0 -0
  60. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/skills/references/command-patterns.md +0 -0
  61. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/skills/references/json-output-format.md +0 -0
  62. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/skills/references/maxcompute-select-guide.md +0 -0
  63. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/skills/references/maxcompute-sql-notes.md +0 -0
  64. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/skills/references/partition-guide.md +0 -0
  65. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/skills/references/red-lines.md +0 -0
  66. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/skills/references/semantic-packages.md +0 -0
  67. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/skills/references/setup-install.md +0 -0
  68. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/skills/references/sql-common-errors.md +0 -0
  69. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/skills/references/sql-query-patterns.md +0 -0
  70. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/skills/references/text2sql-principles.md +0 -0
  71. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/state_permissions.py +0 -0
  72. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli/store.py +0 -0
  73. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli.egg-info/dependency_links.txt +0 -0
  74. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli.egg-info/entry_points.txt +0 -0
  75. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli.egg-info/requires.txt +0 -0
  76. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/src/maxc_cli.egg-info/top_level.txt +0 -0
  77. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_agent_hints_and_cli.py +0 -0
  78. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_agent_platforms.py +0 -0
  79. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_agent_skill_commands.py +0 -0
  80. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_agent_skill_commands_context.py +0 -0
  81. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_ai_native_contract_regressions.py +0 -0
  82. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_auth_logout.py +0 -0
  83. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_backend_auth.py +0 -0
  84. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_backend_data.py +0 -0
  85. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_backend_data_serialization.py +0 -0
  86. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_backend_mcp.py +0 -0
  87. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_backend_meta.py +0 -0
  88. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_build_release_archive_compat.py +0 -0
  89. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_build_release_script.py +0 -0
  90. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_cache.py +0 -0
  91. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_catalog.py +0 -0
  92. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_catalog_bootstrap.py +0 -0
  93. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_cli_arg_validation.py +0 -0
  94. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_cli_mock.py +0 -0
  95. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_cli_query_parse_and_sanitize.py +0 -0
  96. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_compat.py +0 -0
  97. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_config_atomic_write.py +0 -0
  98. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_e2e_smoke.py +0 -0
  99. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_effective_hints_contract.py +0 -0
  100. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_enterprise_tls.py +0 -0
  101. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_envelope_shape.py +0 -0
  102. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_error_self_correction.py +0 -0
  103. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_error_translation.py +0 -0
  104. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_exit_codes.py +0 -0
  105. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_external_auth.py +0 -0
  106. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_flag_hoist.py +0 -0
  107. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_help_format.py +0 -0
  108. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_help_version_e2e.py +0 -0
  109. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_helpers.py +0 -0
  110. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_helpers_csv.py +0 -0
  111. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_installer_contracts.py +0 -0
  112. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_integration.py +0 -0
  113. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_integration_real.py +0 -0
  114. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_job_inspection.py +0 -0
  115. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_job_store_durability.py +0 -0
  116. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_kb_commands.py +0 -0
  117. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_manifest_runtime_contract.py +0 -0
  118. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_masking.py +0 -0
  119. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_mcp_serve.py +0 -0
  120. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_meta_schema_and_partition_cols.py +0 -0
  121. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_odps_runtime.py +0 -0
  122. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_packaging_metadata.py +0 -0
  123. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_phase1_improvements.py +0 -0
  124. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_proxy_auth.py +0 -0
  125. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_pyinstaller_bundle.py +0 -0
  126. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_python39_compat.py +0 -0
  127. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_semantic_management.py +0 -0
  128. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_semantic_scope.py +0 -0
  129. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_semantic_transport.py +0 -0
  130. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_setting_parser.py +0 -0
  131. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_skill_cli_consistency.py +0 -0
  132. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_skill_eval.py +0 -0
  133. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_skill_renderer.py +0 -0
  134. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_startup_imports.py +0 -0
  135. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/tests/test_state_permissions.py +0 -0
  136. {maxc_cli-0.8.0 → maxc_cli-0.8.2}/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.8.0
3
+ Version: 0.8.2
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
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.8.0"
5
+ __version__ = "0.8.2"
@@ -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
- result = self.backend.fetch_job_result(
863
- resolved.instance_id,
864
- project=resolved.project,
865
- max_rows=max_rows,
866
- offset=offset,
867
- session_context=resolved.session_context,
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 = Envelope(
1095
- command=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
- agent_hints=AgentHints(
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
- result = self.backend.fetch_job_result(
1455
- resolved.instance_id,
1456
- project=resolved.project,
1457
- max_rows=100,
1458
- session_context=resolved.session_context,
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
- result = self.backend.fetch_job_result(
1558
- resolved.instance_id,
1559
- project=resolved.project,
1560
- max_rows=max_rows,
1561
- offset=offset,
1562
- session_context=resolved.session_context,
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(["--endpoint", str(endpoint_value or "__MAXC_ENDPOINT__")])
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
- # Resolve credentials first — the picker needs the AK/secret/STS in hand.
4772
- if continuation_id:
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
- persist_login_config(
4987
- target_path,
4988
- auth=persisted_auth,
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="Provide access_id, secret_access_key, security_token, project, and endpoint.",
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="Run `maxc auth login --oauth --project <project> --endpoint <endpoint>`.",
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="Run `maxc auth login` or set the required environment variables.",
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 without exposing its signed query or fragment."""
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(instance.get_logview_address())
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
- records = list(islice(reader, offset, offset + max_rows))
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 = int(getattr(reader, "count", len(rows)) or len(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(rows)
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
- raise translate_odps_error(exc) from exc
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),