maxc-cli 0.8.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.8.0 → maxc_cli-0.8.1}/PKG-INFO +1 -1
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/__init__.py +1 -1
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/_samples.py +4 -3
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/app.py +90 -12
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/auth_providers.py +19 -3
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/cli.py +75 -8
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/config.py +2 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/helpers.py +18 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/oauth.py +12 -5
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/bootstrap-auth.md +22 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli.egg-info/PKG-INFO +1 -1
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli.egg-info/SOURCES.txt +1 -0
- maxc_cli-0.8.1/tests/test_connection_bootstrap.py +279 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_oauth.py +40 -4
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/MANIFEST.in +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/README.md +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/pyproject.toml +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/scripts/pyinstaller_entry.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/scripts/regression_test.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/setup.cfg +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/setup.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/__main__.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/agent_platforms.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/audit.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/auth_continuation.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/backend/__init__.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/backend/auth.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/backend/catalog.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/backend/data.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/backend/job.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/backend/mcp.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/backend/meta.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/backend/odps.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/backend/query.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/backend/semantic.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/cache.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/catalog_bootstrap.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/enterprise_tls.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/exceptions.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/help_format.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/job_ids.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/masking.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/mcp_serve.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/models.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/odps_runtime.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/output.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/proxy_auth.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/semantic.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/semantic_management.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/setting_parser.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/SKILL.md +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/agents/openai.yaml +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/bootstrap-flow.md +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/command-patterns.md +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/json-output-format.md +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/maxcompute-select-guide.md +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/maxcompute-sql-notes.md +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/partition-guide.md +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/red-lines.md +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/semantic-packages.md +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/setup-install.md +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/sql-common-errors.md +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/sql-query-patterns.md +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/text2sql-principles.md +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/state_permissions.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/store.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/utils.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli.egg-info/dependency_links.txt +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli.egg-info/entry_points.txt +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli.egg-info/requires.txt +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli.egg-info/top_level.txt +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_agent_hints_and_cli.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_agent_platforms.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_agent_skill_commands.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_agent_skill_commands_context.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_ai_native_contract_regressions.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_auth_logout.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_backend_auth.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_backend_data.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_backend_data_serialization.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_backend_mcp.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_backend_meta.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_build_release_archive_compat.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_build_release_script.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_cache.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_catalog.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_catalog_bootstrap.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_cli_arg_validation.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_cli_mock.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_cli_query_parse_and_sanitize.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_compat.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_config_atomic_write.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_e2e_smoke.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_effective_hints_contract.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_enterprise_tls.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_envelope_shape.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_error_self_correction.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_error_translation.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_exit_codes.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_external_auth.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_flag_hoist.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_help_format.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_help_version_e2e.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_helpers.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_helpers_csv.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_installer_contracts.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_integration.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_integration_real.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_job_improvements.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_job_inspection.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_job_store_durability.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_kb_commands.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_manifest_runtime_contract.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_masking.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_mcp_serve.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_meta_schema_and_partition_cols.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_odps_runtime.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_output_action_safety.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_output_format_contract.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_packaging_metadata.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_phase1_improvements.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_proxy_auth.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_pyinstaller_bundle.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_python39_compat.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_query_auto_promote.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_query_result_csv_fallback.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_semantic_management.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_semantic_scope.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_semantic_transport.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_setting_parser.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_skill_cli_consistency.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_skill_eval.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_skill_renderer.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_startup_imports.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_state_permissions.py +0 -0
- {maxc_cli-0.8.0 → maxc_cli-0.8.1}/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
|
|
@@ -4708,6 +4715,7 @@ class MaxCApp:
|
|
|
4708
4715
|
projects: 'list[dict[str, Any]]',
|
|
4709
4716
|
*,
|
|
4710
4717
|
title_prefix: 'str',
|
|
4718
|
+
endpoint_flag: 'str' = "--endpoint",
|
|
4711
4719
|
) -> 'list[SuggestedAction]':
|
|
4712
4720
|
actions: list[SuggestedAction] = []
|
|
4713
4721
|
for project_data in projects:
|
|
@@ -4717,7 +4725,7 @@ class MaxCApp:
|
|
|
4717
4725
|
tokens = [*base_tokens, "--project", project_id]
|
|
4718
4726
|
endpoint_value = project_data.get("endpoint")
|
|
4719
4727
|
endpoint_placeholder = endpoint_value is None
|
|
4720
|
-
tokens.extend([
|
|
4728
|
+
tokens.extend([endpoint_flag, str(endpoint_value or "__MAXC_ENDPOINT__")])
|
|
4721
4729
|
if project_data.get("region"):
|
|
4722
4730
|
tokens.extend(["--region", str(project_data["region"])])
|
|
4723
4731
|
if project_data.get("tunnel_endpoint"):
|
|
@@ -4755,6 +4763,8 @@ class MaxCApp:
|
|
|
4755
4763
|
region_name: 'str | None' = None,
|
|
4756
4764
|
tunnel_endpoint: 'str | None' = None,
|
|
4757
4765
|
from_env: 'bool' = False,
|
|
4766
|
+
reuse_auth: 'bool' = False,
|
|
4767
|
+
allow_interactive_picker: 'bool' = True,
|
|
4758
4768
|
no_validate: 'bool' = False,
|
|
4759
4769
|
target_config_path: 'Path | None' = None,
|
|
4760
4770
|
catalog_endpoint: 'str | None' = None,
|
|
@@ -4768,8 +4778,39 @@ class MaxCApp:
|
|
|
4768
4778
|
existing_auth = AuthConfig.from_mapping(existing_payload.get("auth", {}) or {})
|
|
4769
4779
|
env_settings = load_odps_env()
|
|
4770
4780
|
|
|
4771
|
-
#
|
|
4772
|
-
|
|
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:
|
|
4773
4814
|
if any((access_id, secret_access_key, security_token)) or from_env:
|
|
4774
4815
|
raise ValidationError(
|
|
4775
4816
|
"Do not combine an auth continuation with credential flags or --from-env.",
|
|
@@ -4848,6 +4889,7 @@ class MaxCApp:
|
|
|
4848
4889
|
env_settings=env_settings,
|
|
4849
4890
|
existing_auth=existing_auth,
|
|
4850
4891
|
reselect=reselect,
|
|
4892
|
+
allow_interactive=allow_interactive_picker,
|
|
4851
4893
|
)
|
|
4852
4894
|
)
|
|
4853
4895
|
except ProjectPickerPending as exc:
|
|
@@ -4895,6 +4937,18 @@ class MaxCApp:
|
|
|
4895
4937
|
warnings=[],
|
|
4896
4938
|
),
|
|
4897
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
|
|
4898
4952
|
if _oauth_config is not None:
|
|
4899
4953
|
return pending_envelope
|
|
4900
4954
|
if continuation_id:
|
|
@@ -4934,6 +4988,15 @@ class MaxCApp:
|
|
|
4934
4988
|
pending_envelope.metadata["continuation_expires_at_unix"] = expires_at
|
|
4935
4989
|
return pending_envelope
|
|
4936
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
|
+
|
|
4937
5000
|
resolved_auth = AuthConfig(
|
|
4938
5001
|
access_id=resolved_access_id,
|
|
4939
5002
|
secret_access_key=resolved_secret,
|
|
@@ -4983,10 +5046,25 @@ class MaxCApp:
|
|
|
4983
5046
|
persisted_auth.provider = "oauth"
|
|
4984
5047
|
persisted_auth.oauth = _oauth_config
|
|
4985
5048
|
migrate_legacy_session_override(target_path)
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
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)
|
|
4990
5068
|
|
|
4991
5069
|
continuation_cleanup_warning: str | None = None
|
|
4992
5070
|
if continuation_id:
|
|
@@ -5007,7 +5085,7 @@ class MaxCApp:
|
|
|
5007
5085
|
# Always remind callers that AK/SK is stored in plaintext YAML (chmod
|
|
5008
5086
|
# 0600) — flagged in CLAUDE.md as a known limitation. Skip for STS
|
|
5009
5087
|
# tokens since those are short-lived and self-expiring.
|
|
5010
|
-
if not resolved_auth.security_token:
|
|
5088
|
+
if not reuse_auth and not resolved_auth.security_token:
|
|
5011
5089
|
warnings.append(
|
|
5012
5090
|
f"AccessKey saved in plaintext at `{target_path}` (file mode 0600). "
|
|
5013
5091
|
f"For shared/CI environments prefer `auth login-external` with a credential helper, "
|
|
@@ -5017,7 +5095,7 @@ class MaxCApp:
|
|
|
5017
5095
|
warnings.append(
|
|
5018
5096
|
"Credentials were imported from environment variables (--from-env) and saved to config."
|
|
5019
5097
|
)
|
|
5020
|
-
elif any(
|
|
5098
|
+
elif not reuse_auth and any(
|
|
5021
5099
|
env_settings.get(name)
|
|
5022
5100
|
for name in ("access_id", "secret_access_key", "security_token", "endpoint", "region_name", "tunnel_endpoint")
|
|
5023
5101
|
):
|
|
@@ -5055,8 +5133,8 @@ class MaxCApp:
|
|
|
5055
5133
|
|
|
5056
5134
|
login_metadata = {
|
|
5057
5135
|
"config_path": str(target_path),
|
|
5058
|
-
"written_fields": sorted(persisted_auth.to_mapping().keys()),
|
|
5059
|
-
"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",
|
|
5060
5138
|
}
|
|
5061
5139
|
envelope = Envelope(
|
|
5062
5140
|
command="auth.login",
|
|
@@ -5750,7 +5828,7 @@ class MaxCApp:
|
|
|
5750
5828
|
from . import catalog_bootstrap as _catalog_bootstrap
|
|
5751
5829
|
|
|
5752
5830
|
# 2. Picker not viable (non-TTY or --no-picker).
|
|
5753
|
-
if inputs.no_picker or not sys.stdin.isatty():
|
|
5831
|
+
if inputs.no_picker or not inputs.allow_interactive or not sys.stdin.isatty():
|
|
5754
5832
|
# Non-TTY + picker not disabled: list projects for structured output
|
|
5755
5833
|
catalog_warning: str | None = None
|
|
5756
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",
|
|
@@ -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
|
|
|
@@ -786,7 +786,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
786
786
|
"--project",
|
|
787
787
|
help="Target MaxCompute project (omit to pop an interactive picker over the catalog)",
|
|
788
788
|
)
|
|
789
|
-
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
|
+
)
|
|
790
794
|
auth_login.add_argument("--region", dest="region_name", help="MaxCompute region name")
|
|
791
795
|
auth_login.add_argument("--tunnel-endpoint", help="Tunnel endpoint URL for data transfer")
|
|
792
796
|
auth_login.add_argument(
|
|
@@ -1628,11 +1632,14 @@ def run(
|
|
|
1628
1632
|
app: MaxCApp | None = None
|
|
1629
1633
|
try:
|
|
1630
1634
|
_validate_output_request(args, command_name)
|
|
1631
|
-
|
|
1632
|
-
cwd
|
|
1633
|
-
config_path
|
|
1634
|
-
load_backend
|
|
1635
|
-
|
|
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)
|
|
1636
1643
|
args.handler(app, args, stdout)
|
|
1637
1644
|
return getattr(args, "_envelope_exit_code", 0)
|
|
1638
1645
|
except MaxCError as exc:
|
|
@@ -1702,6 +1709,40 @@ def run(
|
|
|
1702
1709
|
),
|
|
1703
1710
|
}
|
|
1704
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])
|
|
1705
1746
|
# Build schema context for SQL errors to enable agent self-correction
|
|
1706
1747
|
if app is not None and exc.error_code in (
|
|
1707
1748
|
"SQL_ERROR", "NOT_FOUND", "SCHEMA_NOT_FOUND", "TABLE_NOT_FOUND", "COLUMN_NOT_FOUND",
|
|
@@ -1725,6 +1766,10 @@ def run(
|
|
|
1725
1766
|
error=exc.to_payload(),
|
|
1726
1767
|
agent_hints=_hints,
|
|
1727
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
|
+
]
|
|
1728
1773
|
if _is_job_command(args):
|
|
1729
1774
|
_prepare_job_failure_envelope(payload, args)
|
|
1730
1775
|
if _is_job_wait_stream(args):
|
|
@@ -2440,6 +2485,14 @@ def _handle_auth_login(app: MaxCApp, args: argparse.Namespace, stdout: TextIO) -
|
|
|
2440
2485
|
args.security_token,
|
|
2441
2486
|
)
|
|
2442
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
|
+
)
|
|
2443
2496
|
if args.oauth:
|
|
2444
2497
|
if args.from_env or explicit_credentials:
|
|
2445
2498
|
raise ValidationError(
|
|
@@ -2506,6 +2559,8 @@ def _handle_auth_login(app: MaxCApp, args: argparse.Namespace, stdout: TextIO) -
|
|
|
2506
2559
|
region_name=args.region_name,
|
|
2507
2560
|
tunnel_endpoint=args.tunnel_endpoint,
|
|
2508
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),
|
|
2509
2564
|
no_validate=args.no_validate,
|
|
2510
2565
|
target_config_path=args.requested_config_path,
|
|
2511
2566
|
catalog_endpoint=args.catalog_endpoint,
|
|
@@ -2721,6 +2776,14 @@ def _manifest_requirements(command: str) -> dict[str, Any]:
|
|
|
2721
2776
|
"mode": "required",
|
|
2722
2777
|
"reason": "A new OAuth authorization and STS exchange use network APIs.",
|
|
2723
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
|
+
},
|
|
2724
2787
|
{
|
|
2725
2788
|
"when": _manifest_condition("no_validate", equals=False),
|
|
2726
2789
|
"mode": "required",
|
|
@@ -2968,12 +3031,16 @@ def _manifest_effects(command: str) -> list[dict[str, Any]]:
|
|
|
2968
3031
|
"create_or_replace",
|
|
2969
3032
|
"auth_config",
|
|
2970
3033
|
when={"runtime": "login_succeeds"},
|
|
3034
|
+
note="--reuse-auth preserves the existing identity and writes connection fields only.",
|
|
2971
3035
|
),
|
|
2972
3036
|
_manifest_effect(
|
|
2973
3037
|
"local",
|
|
2974
3038
|
"create",
|
|
2975
3039
|
"owner_only_access_key_continuation",
|
|
2976
|
-
when={"
|
|
3040
|
+
when={"all": [
|
|
3041
|
+
{"runtime": "access_key_project_selection_pending"},
|
|
3042
|
+
_manifest_condition("reuse_auth", equals=False),
|
|
3043
|
+
]},
|
|
2977
3044
|
),
|
|
2978
3045
|
_manifest_effect(
|
|
2979
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':
|
|
@@ -280,10 +280,28 @@ def resolve_odps_settings(
|
|
|
280
280
|
suppressed_env_vars.append(field)
|
|
281
281
|
else:
|
|
282
282
|
for field, env_value in env_settings.items():
|
|
283
|
+
if (field == "region_name" and settings.get(field)
|
|
284
|
+
and os.environ.get("MAXC_CLI_NAME") == "aliyun maxc"):
|
|
285
|
+
# The wrapper's profile region is a default, not a request to
|
|
286
|
+
# reroute the Catalog project selected in connection setup.
|
|
287
|
+
continue
|
|
283
288
|
if env_value:
|
|
284
289
|
settings[field] = env_value
|
|
285
290
|
sources[field] = "environment"
|
|
286
291
|
|
|
292
|
+
if auth_override is None and getattr(config, "command_project", None):
|
|
293
|
+
settings["project"] = config.command_project
|
|
294
|
+
sources["project"] = "command_line"
|
|
295
|
+
|
|
296
|
+
# The wrapper passes the selected Alibaba Cloud profile's region. Derive
|
|
297
|
+
# the public data-plane endpoint only when no endpoint was selected.
|
|
298
|
+
if not settings.get("endpoint") and settings.get("region_name"):
|
|
299
|
+
from .catalog_bootstrap import region_to_endpoint
|
|
300
|
+
|
|
301
|
+
settings["endpoint"] = region_to_endpoint(settings["region_name"])
|
|
302
|
+
if settings["endpoint"]:
|
|
303
|
+
sources["endpoint"] = sources.get("region_name", "config_file")
|
|
304
|
+
|
|
287
305
|
return settings, sources, suppressed_env_vars
|
|
288
306
|
|
|
289
307
|
|
|
@@ -533,10 +533,17 @@ def exchange_sts(
|
|
|
533
533
|
"securityToken",
|
|
534
534
|
"expiration",
|
|
535
535
|
)
|
|
536
|
+
# Match aliyun-cli's Go JSON decoder: prefer the exact tagged name, then
|
|
537
|
+
# accept a case-insensitive match (the service also returns PascalCase).
|
|
538
|
+
folded_payload = {key.lower(): value for key, value in payload.items()}
|
|
539
|
+
credentials = {
|
|
540
|
+
field: payload.get(field, folded_payload.get(field.lower()))
|
|
541
|
+
for field in required_fields
|
|
542
|
+
}
|
|
536
543
|
missing = [
|
|
537
544
|
field
|
|
538
545
|
for field in required_fields
|
|
539
|
-
if not isinstance(
|
|
546
|
+
if not isinstance(credentials[field], str) or not credentials[field]
|
|
540
547
|
]
|
|
541
548
|
if missing:
|
|
542
549
|
raise OAuthError(
|
|
@@ -545,10 +552,10 @@ def exchange_sts(
|
|
|
545
552
|
+ "."
|
|
546
553
|
)
|
|
547
554
|
return StsCredential(
|
|
548
|
-
access_key_id=
|
|
549
|
-
access_key_secret=
|
|
550
|
-
security_token=
|
|
551
|
-
expiration_iso=
|
|
555
|
+
access_key_id=credentials["accessKeyId"],
|
|
556
|
+
access_key_secret=credentials["accessKeySecret"],
|
|
557
|
+
security_token=credentials["securityToken"],
|
|
558
|
+
expiration_iso=credentials["expiration"],
|
|
552
559
|
)
|
|
553
560
|
|
|
554
561
|
|
|
@@ -114,6 +114,28 @@ secret in the response.
|
|
|
114
114
|
|
|
115
115
|
## Project And Endpoint
|
|
116
116
|
|
|
117
|
+
A saved default project is optional. With an existing identity, pass the
|
|
118
|
+
intended project on each data command, for example:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
{{cli}} query "SELECT 1" --project <project> --json
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
If neither `--project` nor a configured/environment default exists, the command
|
|
125
|
+
returns `VALIDATION_ERROR` with a retry template containing `--project`. It
|
|
126
|
+
never opens a project picker or changes the connection automatically. Retain
|
|
127
|
+
the original Alibaba Cloud `--profile` on recovery commands.
|
|
128
|
+
|
|
129
|
+
To configure a default explicitly, run `{{cli}} auth login --reuse-auth`. This
|
|
130
|
+
reuses the Catalog picker and saves connection fields only. The JSON variant
|
|
131
|
+
returns project actions without prompting or starting new OAuth authorization;
|
|
132
|
+
a wrapper profile continues to own and refresh its credentials.
|
|
133
|
+
|
|
134
|
+
A known profile Region supplies the default public data-plane endpoint; a
|
|
135
|
+
Catalog project selected during explicit setup supplies its actual Region.
|
|
136
|
+
Explicit `--odps-endpoint` on `auth login` overrides the endpoint and works
|
|
137
|
+
through the wrapper. Unknown regions require an endpoint from the user.
|
|
138
|
+
|
|
117
139
|
- Use values supplied by the profile, Catalog result, current configuration, or
|
|
118
140
|
the user.
|
|
119
141
|
- Never invent an endpoint or derive a related project from a name suffix.
|
|
@@ -86,6 +86,7 @@ tests/test_cli_mock.py
|
|
|
86
86
|
tests/test_cli_query_parse_and_sanitize.py
|
|
87
87
|
tests/test_compat.py
|
|
88
88
|
tests/test_config_atomic_write.py
|
|
89
|
+
tests/test_connection_bootstrap.py
|
|
89
90
|
tests/test_e2e_smoke.py
|
|
90
91
|
tests/test_effective_hints_contract.py
|
|
91
92
|
tests/test_enterprise_tls.py
|