maxc-cli 0.8.1__tar.gz → 0.9.0__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 (139) hide show
  1. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/PKG-INFO +14 -2
  2. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/README.md +13 -1
  3. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/__init__.py +1 -1
  4. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/_samples.py +8 -0
  5. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/app.py +159 -35
  6. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/backend/job.py +4 -2
  7. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/backend/meta.py +94 -0
  8. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/backend/odps.py +18 -4
  9. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/backend/query.py +0 -1
  10. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/cli.py +49 -2
  11. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/exceptions.py +15 -1
  12. maxc_cli-0.9.0/src/maxc_cli/function_metadata.py +36 -0
  13. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/helpers.py +17 -1
  14. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/models.py +10 -1
  15. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/output.py +21 -4
  16. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/skills/SKILL.md +14 -2
  17. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/skills/references/command-patterns.md +7 -0
  18. maxc_cli-0.9.0/src/maxc_cli/skills/references/registered-functions.md +78 -0
  19. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/utils.py +42 -7
  20. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli.egg-info/PKG-INFO +14 -2
  21. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli.egg-info/SOURCES.txt +3 -0
  22. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_help_version_e2e.py +8 -0
  23. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_job_improvements.py +52 -0
  24. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_output_action_safety.py +25 -0
  25. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_output_format_contract.py +15 -0
  26. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_query_auto_promote.py +96 -0
  27. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_query_result_csv_fallback.py +26 -2
  28. maxc_cli-0.9.0/tests/test_registered_functions.py +207 -0
  29. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/MANIFEST.in +0 -0
  30. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/pyproject.toml +0 -0
  31. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/scripts/pyinstaller_entry.py +0 -0
  32. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/scripts/regression_test.py +0 -0
  33. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/setup.cfg +0 -0
  34. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/setup.py +0 -0
  35. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/__main__.py +0 -0
  36. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/agent_platforms.py +0 -0
  37. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/audit.py +0 -0
  38. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/auth_continuation.py +0 -0
  39. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/auth_providers.py +0 -0
  40. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/backend/__init__.py +0 -0
  41. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/backend/auth.py +0 -0
  42. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/backend/catalog.py +0 -0
  43. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/backend/data.py +0 -0
  44. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/backend/mcp.py +0 -0
  45. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/backend/semantic.py +0 -0
  46. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/cache.py +0 -0
  47. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/catalog_bootstrap.py +0 -0
  48. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/config.py +0 -0
  49. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/enterprise_tls.py +0 -0
  50. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/help_format.py +0 -0
  51. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/job_ids.py +0 -0
  52. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/masking.py +0 -0
  53. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/mcp_serve.py +0 -0
  54. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/oauth.py +0 -0
  55. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/odps_runtime.py +0 -0
  56. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/proxy_auth.py +0 -0
  57. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/semantic.py +0 -0
  58. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/semantic_management.py +0 -0
  59. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/setting_parser.py +0 -0
  60. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/skills/agents/openai.yaml +0 -0
  61. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/skills/references/bootstrap-auth.md +0 -0
  62. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/skills/references/bootstrap-flow.md +0 -0
  63. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/skills/references/json-output-format.md +0 -0
  64. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/skills/references/maxcompute-select-guide.md +0 -0
  65. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/skills/references/maxcompute-sql-notes.md +0 -0
  66. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/skills/references/partition-guide.md +0 -0
  67. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/skills/references/red-lines.md +0 -0
  68. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/skills/references/semantic-packages.md +0 -0
  69. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/skills/references/setup-install.md +0 -0
  70. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/skills/references/sql-common-errors.md +0 -0
  71. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/skills/references/sql-query-patterns.md +0 -0
  72. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/skills/references/text2sql-principles.md +0 -0
  73. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/state_permissions.py +0 -0
  74. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli/store.py +0 -0
  75. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli.egg-info/dependency_links.txt +0 -0
  76. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli.egg-info/entry_points.txt +0 -0
  77. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli.egg-info/requires.txt +0 -0
  78. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/src/maxc_cli.egg-info/top_level.txt +0 -0
  79. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_agent_hints_and_cli.py +0 -0
  80. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_agent_platforms.py +0 -0
  81. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_agent_skill_commands.py +0 -0
  82. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_agent_skill_commands_context.py +0 -0
  83. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_ai_native_contract_regressions.py +0 -0
  84. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_auth_logout.py +0 -0
  85. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_backend_auth.py +0 -0
  86. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_backend_data.py +0 -0
  87. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_backend_data_serialization.py +0 -0
  88. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_backend_mcp.py +0 -0
  89. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_backend_meta.py +0 -0
  90. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_build_release_archive_compat.py +0 -0
  91. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_build_release_script.py +0 -0
  92. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_cache.py +0 -0
  93. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_catalog.py +0 -0
  94. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_catalog_bootstrap.py +0 -0
  95. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_cli_arg_validation.py +0 -0
  96. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_cli_mock.py +0 -0
  97. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_cli_query_parse_and_sanitize.py +0 -0
  98. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_compat.py +0 -0
  99. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_config_atomic_write.py +0 -0
  100. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_connection_bootstrap.py +0 -0
  101. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_e2e_smoke.py +0 -0
  102. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_effective_hints_contract.py +0 -0
  103. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_enterprise_tls.py +0 -0
  104. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_envelope_shape.py +0 -0
  105. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_error_self_correction.py +0 -0
  106. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_error_translation.py +0 -0
  107. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_exit_codes.py +0 -0
  108. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_external_auth.py +0 -0
  109. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_flag_hoist.py +0 -0
  110. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_help_format.py +0 -0
  111. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_helpers.py +0 -0
  112. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_helpers_csv.py +0 -0
  113. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_installer_contracts.py +0 -0
  114. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_integration.py +0 -0
  115. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_integration_real.py +0 -0
  116. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_job_inspection.py +0 -0
  117. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_job_store_durability.py +0 -0
  118. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_kb_commands.py +0 -0
  119. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_manifest_runtime_contract.py +0 -0
  120. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_masking.py +0 -0
  121. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_mcp_serve.py +0 -0
  122. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_meta_schema_and_partition_cols.py +0 -0
  123. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_oauth.py +0 -0
  124. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_odps_runtime.py +0 -0
  125. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_packaging_metadata.py +0 -0
  126. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_phase1_improvements.py +0 -0
  127. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_proxy_auth.py +0 -0
  128. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_pyinstaller_bundle.py +0 -0
  129. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_python39_compat.py +0 -0
  130. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_semantic_management.py +0 -0
  131. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_semantic_scope.py +0 -0
  132. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_semantic_transport.py +0 -0
  133. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_setting_parser.py +0 -0
  134. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_skill_cli_consistency.py +0 -0
  135. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_skill_eval.py +0 -0
  136. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_skill_renderer.py +0 -0
  137. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_startup_imports.py +0 -0
  138. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/tests/test_state_permissions.py +0 -0
  139. {maxc_cli-0.8.1 → maxc_cli-0.9.0}/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.1
3
+ Version: 0.9.0
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
@@ -70,7 +70,7 @@ maxc agent doctor --online --json
70
70
  |------|------|------|
71
71
  | **query** | `query [run]`, `query cost`, `query explain` | SQL 执行、成本估算、执行计划 |
72
72
  | **job** | `submit`, `status`, `wait`, `result`, `cancel`, `diagnose`, `list` | 异步任务全生命周期 |
73
- | **meta** | `list-tables`, `describe`, `search`, `search-columns`, `partitions`, `latest-partition`, `freshness`, `list-projects`, `list-schemas`, `semantic set/get/clear/list-missing` | 元数据发现与语义管理 |
73
+ | **meta** | `list-tables`, `describe`, `list-functions`, `describe-function`, `search`, `search-columns`, `partitions`, `latest-partition`, `freshness`, `list-projects`, `list-schemas`, `semantic set/get/clear/list-missing` | 元数据发现与语义管理 |
74
74
  | **data** | `sample`, `profile`, `upload`, `download` | 数据采样、画像与 CSV/TSV 传输 |
75
75
  | **auth** | `login`, `login-external`, `logout`, `whoami`, `can-i` | 认证与权限 |
76
76
  | **session** | `set`, `show`, `unset` | 项目/Schema 切换 |
@@ -81,6 +81,18 @@ maxc agent doctor --online --json
81
81
  使用 `--json`;CSV/NDJSON 行流和 `job wait --stream` 生命周期流是明确例外,
82
82
  不会为每条记录重复封装 Envelope。
83
83
 
84
+ 0.9.0 起支持已注册函数的目录与详情查询:
85
+
86
+ ```bash
87
+ aliyun maxc meta list-functions --project <project> --prefix <prefix> --limit 20 --json
88
+ aliyun maxc meta describe-function <alias> --project <project> --json
89
+ ```
90
+
91
+ 三层项目可加 `--schema`;详情参数使用裸函数别名。列表有界分页,详情保留
92
+ 类名和原始资源引用,不下载资源内容,也不推断签名或 Python 版本。
93
+ 按作者文档确定调用契约后,复用现有 SQL 成本、执行和作业命令引用函数。
94
+ 具体流程见 [已注册函数](src/maxc_cli/skills/references/registered-functions.md)。
95
+
84
96
  ## Agent 集成
85
97
 
86
98
  ### 方式 1:公共 Skill(主路径)
@@ -48,7 +48,7 @@ maxc agent doctor --online --json
48
48
  |------|------|------|
49
49
  | **query** | `query [run]`, `query cost`, `query explain` | SQL 执行、成本估算、执行计划 |
50
50
  | **job** | `submit`, `status`, `wait`, `result`, `cancel`, `diagnose`, `list` | 异步任务全生命周期 |
51
- | **meta** | `list-tables`, `describe`, `search`, `search-columns`, `partitions`, `latest-partition`, `freshness`, `list-projects`, `list-schemas`, `semantic set/get/clear/list-missing` | 元数据发现与语义管理 |
51
+ | **meta** | `list-tables`, `describe`, `list-functions`, `describe-function`, `search`, `search-columns`, `partitions`, `latest-partition`, `freshness`, `list-projects`, `list-schemas`, `semantic set/get/clear/list-missing` | 元数据发现与语义管理 |
52
52
  | **data** | `sample`, `profile`, `upload`, `download` | 数据采样、画像与 CSV/TSV 传输 |
53
53
  | **auth** | `login`, `login-external`, `logout`, `whoami`, `can-i` | 认证与权限 |
54
54
  | **session** | `set`, `show`, `unset` | 项目/Schema 切换 |
@@ -59,6 +59,18 @@ maxc agent doctor --online --json
59
59
  使用 `--json`;CSV/NDJSON 行流和 `job wait --stream` 生命周期流是明确例外,
60
60
  不会为每条记录重复封装 Envelope。
61
61
 
62
+ 0.9.0 起支持已注册函数的目录与详情查询:
63
+
64
+ ```bash
65
+ aliyun maxc meta list-functions --project <project> --prefix <prefix> --limit 20 --json
66
+ aliyun maxc meta describe-function <alias> --project <project> --json
67
+ ```
68
+
69
+ 三层项目可加 `--schema`;详情参数使用裸函数别名。列表有界分页,详情保留
70
+ 类名和原始资源引用,不下载资源内容,也不推断签名或 Python 版本。
71
+ 按作者文档确定调用契约后,复用现有 SQL 成本、执行和作业命令引用函数。
72
+ 具体流程见 [已注册函数](src/maxc_cli/skills/references/registered-functions.md)。
73
+
62
74
  ## Agent 集成
63
75
 
64
76
  ### 方式 1:公共 Skill(主路径)
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.8.1"
5
+ __version__ = "0.9.0"
@@ -85,6 +85,14 @@ SAMPLES: dict[str, str] = {
85
85
  "maxc meta describe default.orders --project my_proj\n"
86
86
  "maxc meta describe default.orders --project my_proj --full --json"
87
87
  ),
88
+ "meta.list-functions": (
89
+ "maxc meta list-functions --project my_proj --prefix normalize_ --limit 20 --json\n"
90
+ "maxc meta list-functions --project my_proj --schema default --limit 20 --json"
91
+ ),
92
+ "meta.describe-function": (
93
+ "maxc meta describe-function normalize_phone --project my_proj --json\n"
94
+ "maxc meta describe-function normalize_phone --project my_proj --schema default --json"
95
+ ),
88
96
  "meta.search": (
89
97
  "maxc meta search orders\n"
90
98
  "maxc meta search user --project my_proj --json"
@@ -44,6 +44,7 @@ from .exceptions import (
44
44
  TwoTierNamespaceError,
45
45
  ValidationError,
46
46
  )
47
+ from .function_metadata import function_cursor, function_offset
47
48
  from .helpers import (
48
49
  build_odps_identity_payload,
49
50
  build_task_summary,
@@ -866,13 +867,22 @@ class MaxCApp:
866
867
  session["job_id"],
867
868
  project=session.get("project") or None,
868
869
  )
869
- result = self.backend.fetch_job_result(
870
- resolved.instance_id,
871
- project=resolved.project,
872
- max_rows=max_rows,
873
- offset=offset,
874
- session_context=resolved.session_context,
875
- )
870
+ try:
871
+ result = self.backend.fetch_job_result(
872
+ resolved.instance_id,
873
+ project=resolved.project,
874
+ max_rows=max_rows,
875
+ offset=offset,
876
+ session_context=resolved.session_context,
877
+ )
878
+ except Exception as exc:
879
+ error = exc if isinstance(exc, MaxCError) else translate_odps_error(exc)
880
+ envelope = self._job_result_fetch_failure(
881
+ command, resolved, None, error.to_payload(),
882
+ max_rows=max_rows, cursor=cursor,
883
+ )
884
+ self.log(command, envelope.status, envelope.metadata)
885
+ return envelope
876
886
  envelope = self._build_query_envelope(
877
887
  command=command,
878
888
  result=result,
@@ -1098,25 +1108,13 @@ class MaxCApp:
1098
1108
  except Exception as exc:
1099
1109
  fetch_error = translate_odps_error(exc).to_payload()
1100
1110
  if fetch_error is not None:
1101
- envelope = Envelope(
1102
- command=command,
1103
- status="failure",
1104
- data=None,
1105
- error=fetch_error,
1111
+ envelope = self._job_result_fetch_failure(
1112
+ command, resolved, job_info, fetch_error, max_rows=max_rows,
1106
1113
  metadata={
1107
- "job_id": resolved.external_job_id,
1108
- "project": resolved.project,
1109
- "logview": job_info.logview,
1110
1114
  "sql_executed": sql,
1111
1115
  **execution_metadata,
1112
1116
  },
1113
- agent_hints=AgentHints(
1114
- actions=[
1115
- action("job.result", data={"job_id": resolved.external_job_id}, metadata={"job_id": resolved.external_job_id, "project": resolved.project, "sql_executed": sql}),
1116
- action("job.status", data={"job_id": resolved.external_job_id}, metadata={"job_id": resolved.external_job_id, "project": resolved.project}),
1117
- ],
1118
- warnings=common_warnings,
1119
- ),
1117
+ warnings=common_warnings,
1120
1118
  )
1121
1119
  if idempotency_key:
1122
1120
  envelope.metadata["idempotency_key"] = idempotency_key
@@ -1458,12 +1456,20 @@ class MaxCApp:
1458
1456
  ]
1459
1457
  self.log("job.wait", envelope.status, envelope.metadata)
1460
1458
  return envelope, events
1461
- result = self.backend.fetch_job_result(
1462
- resolved.instance_id,
1463
- project=resolved.project,
1464
- max_rows=100,
1465
- session_context=resolved.session_context,
1466
- )
1459
+ try:
1460
+ result = self.backend.fetch_job_result(
1461
+ resolved.instance_id,
1462
+ project=resolved.project,
1463
+ max_rows=100,
1464
+ session_context=resolved.session_context,
1465
+ )
1466
+ except Exception as exc:
1467
+ error = exc if isinstance(exc, MaxCError) else translate_odps_error(exc)
1468
+ envelope = self._job_result_fetch_failure(
1469
+ "job.wait", resolved, after, error.to_payload(), max_rows=100,
1470
+ )
1471
+ self.log("job.wait", envelope.status, envelope.metadata)
1472
+ return envelope, []
1467
1473
  envelope = self._build_query_envelope(
1468
1474
  command="job.wait",
1469
1475
  result=result,
@@ -1561,13 +1567,22 @@ class MaxCApp:
1561
1567
  )
1562
1568
  self.log("job.result", envelope.status, envelope.metadata)
1563
1569
  return envelope
1564
- result = self.backend.fetch_job_result(
1565
- resolved.instance_id,
1566
- project=resolved.project,
1567
- max_rows=max_rows,
1568
- offset=offset,
1569
- session_context=resolved.session_context,
1570
- )
1570
+ try:
1571
+ result = self.backend.fetch_job_result(
1572
+ resolved.instance_id,
1573
+ project=resolved.project,
1574
+ max_rows=max_rows,
1575
+ offset=offset,
1576
+ session_context=resolved.session_context,
1577
+ )
1578
+ except Exception as exc:
1579
+ error = exc if isinstance(exc, MaxCError) else translate_odps_error(exc)
1580
+ envelope = self._job_result_fetch_failure(
1581
+ "job.result", resolved, info, error.to_payload(),
1582
+ max_rows=max_rows, cursor=cursor,
1583
+ )
1584
+ self.log("job.result", envelope.status, envelope.metadata)
1585
+ return envelope
1571
1586
  envelope = self._build_query_envelope(
1572
1587
  command="job.result",
1573
1588
  result=result,
@@ -1669,6 +1684,48 @@ class MaxCApp:
1669
1684
  self.log("job.result", envelope.status, envelope.metadata)
1670
1685
  return envelope
1671
1686
 
1687
+ def _job_result_fetch_failure(
1688
+ self,
1689
+ command: 'str',
1690
+ resolved: '_ResolvedExternalJobId',
1691
+ info: 'JobInfo | None',
1692
+ error: 'ErrorPayload',
1693
+ *,
1694
+ max_rows: 'int',
1695
+ cursor: 'str | None' = None,
1696
+ metadata: 'dict[str, Any] | None' = None,
1697
+ warnings: 'list[str] | None' = None,
1698
+ ) -> 'Envelope':
1699
+ """Keep the existing job and page observable when reading fails."""
1700
+ scope = {
1701
+ "job_id": resolved.external_job_id,
1702
+ "project": resolved.project,
1703
+ "logview": info.logview if info else error.logview,
1704
+ **({
1705
+ "job_status": info.status,
1706
+ "submitted_at": info.submitted_at,
1707
+ "completed_at": info.completed_at,
1708
+ } if info else {}),
1709
+ **(metadata or {}),
1710
+ }
1711
+ retry = action("job.result", data={"max_rows": max_rows, "cursor": cursor}, metadata=scope)
1712
+ error.instance_id = resolved.external_job_id
1713
+ error.logview = scope["logview"]
1714
+ error.recovery_steps = [
1715
+ "Retry reading the existing job result: " + retry.command,
1716
+ "Do not resubmit the SQL to recover a result-reader failure.",
1717
+ ]
1718
+ return Envelope(
1719
+ command=command,
1720
+ status="failure",
1721
+ error=error,
1722
+ metadata=scope,
1723
+ agent_hints=AgentHints(
1724
+ actions=[retry, action("job.diagnose", metadata=scope)],
1725
+ warnings=warnings or [],
1726
+ ),
1727
+ )
1728
+
1672
1729
  def _bound_job_result_cursor(
1673
1730
  self,
1674
1731
  cursor: 'str | None',
@@ -1967,6 +2024,73 @@ class MaxCApp:
1967
2024
  self.log("job.list", envelope.status, envelope.metadata)
1968
2025
  return envelope
1969
2026
 
2027
+ def meta_list_functions(
2028
+ self,
2029
+ *,
2030
+ schema: 'str | None' = None,
2031
+ project: 'str | None' = None,
2032
+ prefix: 'str | None' = None,
2033
+ limit: int = 50,
2034
+ cursor: 'str | None' = None,
2035
+ ) -> Envelope:
2036
+ if type(limit) is not int or not 1 <= limit <= 1000:
2037
+ raise ValidationError("Function list limit must be between 1 and 1000.")
2038
+ target_project = project or self.config.default_project
2039
+ effective_schema = schema or self.config.default_schema
2040
+ offset = function_offset(cursor, project=target_project, schema=effective_schema, prefix=prefix)
2041
+ rows, has_more = self.backend.list_functions(
2042
+ project=target_project, schema=effective_schema, prefix=prefix, limit=limit, offset=offset,
2043
+ )
2044
+ data = {
2045
+ "functions": [dict(row, project=target_project, schema_name=effective_schema) for row in rows],
2046
+ "pagination": {
2047
+ "returned_count": len(rows),
2048
+ "has_more": has_more,
2049
+ "limit": limit,
2050
+ "offset": offset,
2051
+ "next_cursor": function_cursor(
2052
+ offset + len(rows), project=target_project, schema=effective_schema, prefix=prefix,
2053
+ ) if has_more else None,
2054
+ },
2055
+ }
2056
+ metadata = {"project": target_project, "schema": effective_schema, "prefix": prefix}
2057
+ envelope = Envelope(
2058
+ command="meta.list-functions", status="success", data=data, metadata=metadata,
2059
+ agent_hints=AgentHints(warnings=[
2060
+ "Catalog pages are live and not a snapshot; registrations can change between calls.",
2061
+ "Function aliases are metadata names, not SQL-qualified references. Verify namespace settings before constructing SQL.",
2062
+ ]),
2063
+ )
2064
+ self.log(envelope.command, envelope.status, metadata)
2065
+ return envelope
2066
+
2067
+ def meta_describe_function(
2068
+ self,
2069
+ function_name: str,
2070
+ *,
2071
+ schema: 'str | None' = None,
2072
+ project: 'str | None' = None,
2073
+ ) -> Envelope:
2074
+ if not function_name or any(char.isspace() or char in ".:/\\" for char in function_name):
2075
+ raise ValidationError(
2076
+ "Pass a bare registered function alias.",
2077
+ suggestion="Select the function's project and schema with --project and --schema.",
2078
+ )
2079
+ target_project = project or self.config.default_project
2080
+ effective_schema = schema or self.config.default_schema
2081
+ detail = self.backend.describe_function(function_name, project=target_project, schema=effective_schema)
2082
+ detail = dict(detail, project=target_project, schema_name=effective_schema)
2083
+ metadata = {"project": target_project, "schema": effective_schema}
2084
+ envelope = Envelope(
2085
+ command="meta.describe-function", status="success", data={"function": detail}, metadata=metadata,
2086
+ agent_hints=AgentHints(warnings=[
2087
+ "Registration metadata does not establish the input/output signature or runtime version. Use the function owner's documentation or verified user context; do not infer them from class or resource names.",
2088
+ "Metadata Read and SQL Execute are separate permissions. A failed describe is not proof that an already-known function cannot be executed.",
2089
+ ]),
2090
+ )
2091
+ self.log(envelope.command, envelope.status, metadata)
2092
+ return envelope
2093
+
1970
2094
  def meta_list_tables(
1971
2095
  self,
1972
2096
  *,
@@ -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
@@ -20,6 +20,100 @@ from ..helpers import (
20
20
  class MetaMixin:
21
21
  """Mixin providing metadata methods."""
22
22
 
23
+ def list_functions(
24
+ self,
25
+ *,
26
+ project: 'str | None' = None,
27
+ schema: 'str | None' = None,
28
+ prefix: 'str | None' = None,
29
+ limit: int = 50,
30
+ offset: int = 0,
31
+ ) -> 'tuple[list[dict[str, Any]], bool]':
32
+ """Read a bounded catalog window without per-function metadata GETs.
33
+
34
+ Args:
35
+ project: Function project, or the configured project.
36
+ schema: Explicit function schema, when applicable.
37
+ prefix: Server-side alias prefix filter.
38
+ limit: Maximum returned functions.
39
+ offset: Number of earlier matching catalog entries to skip.
40
+
41
+ Returns:
42
+ Parsed collection rows and whether another matching entry exists.
43
+ """
44
+ try:
45
+ iterator = iter(self.client.list_functions(
46
+ project=project or self.project, schema=schema, prefix=prefix,
47
+ ))
48
+ window = list(islice(iterator, offset, offset + limit + 1))
49
+ # PyODPS models lazily reload missing fields. Read only parsed
50
+ # collection fields here so List does not also require Read.
51
+ rows = [{
52
+ "function_name": object.__getattribute__(item, "name"),
53
+ "class_type": object.__getattribute__(item, "class_type"),
54
+ "owner": object.__getattribute__(item, "_owner"),
55
+ } for item in window[:limit]]
56
+ return rows, len(window) > limit
57
+ except Exception as exc:
58
+ raise translate_odps_error(exc, "list_functions") from exc
59
+
60
+ def describe_function(
61
+ self,
62
+ function_name: str,
63
+ *,
64
+ project: 'str | None' = None,
65
+ schema: 'str | None' = None,
66
+ ) -> 'dict[str, Any]':
67
+ """Read registration metadata without loading resource contents.
68
+
69
+ Args:
70
+ function_name: Bare registered function alias.
71
+ project: Function project, or the configured project.
72
+ schema: Explicit function schema, when applicable.
73
+
74
+ Returns:
75
+ Registration metadata with exact resource references and unknown
76
+ signature/runtime fields. Implementation code is excluded.
77
+ """
78
+ try:
79
+ function = self.client.get_function(
80
+ function_name, project=project or self.project, schema=schema,
81
+ )
82
+ # get_function is lazy: reload explicitly so missing aliases fail
83
+ # even when the SDK already knows the requested name.
84
+ function.reload()
85
+
86
+ def read(key: str) -> Any:
87
+ try:
88
+ return object.__getattribute__(function, key)
89
+ except AttributeError:
90
+ # Older supported PyODPS releases omit optional flags.
91
+ return None
92
+
93
+ return {
94
+ "function_name": read("name"),
95
+ "class_type": read("class_type"),
96
+ "resource_names": list(read("_resources") or []),
97
+ "owner": read("_owner"),
98
+ "creation_time": _dt_to_iso(read("creation_time")),
99
+ "program_language": read("program_language"),
100
+ "is_sql_function": self._function_boolean(read("is_sql_function")),
101
+ "is_embedded_function": self._function_boolean(read("is_embedded_function")),
102
+ "file_name": read("file_name"),
103
+ "signature": None,
104
+ "runtime_version": None,
105
+ }
106
+ except Exception as exc:
107
+ raise translate_odps_error(exc, "describe_function") from exc
108
+
109
+ @staticmethod
110
+ def _function_boolean(value: Any) -> 'bool | None':
111
+ if value is True or str(value).lower() == "true":
112
+ return True
113
+ if value is False or str(value).lower() == "false":
114
+ return False
115
+ return None
116
+
23
117
  def list_tables(
24
118
  self,
25
119
  *,
@@ -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),
@@ -455,6 +455,22 @@ def build_parser() -> argparse.ArgumentParser:
455
455
  meta_describe.add_argument("--full", action="store_true", help="Show full column list (default is summary mode)")
456
456
  meta_describe.set_defaults(handler=_handle_meta_describe)
457
457
 
458
+ meta_functions = _make_parser(meta_subparsers, "list-functions", "meta.list-functions", help="List registered functions")
459
+ meta_functions.add_argument("--project", help="Target MaxCompute project")
460
+ meta_functions.add_argument("--schema", help="Schema name (overrides session default)")
461
+ meta_functions.add_argument("--prefix", help="Function alias prefix filter")
462
+ meta_functions.add_argument("--limit", type=positive_int, default=50, help="Maximum functions to return (1-1000; default 50)")
463
+ meta_functions.add_argument("--cursor", help="Scope-bound cursor returned by the previous function list")
464
+ meta_functions.add_argument("--json", action="store_true", help="Output as JSON envelope")
465
+ meta_functions.set_defaults(handler=_handle_meta_list_functions)
466
+
467
+ meta_function = _make_parser(meta_subparsers, "describe-function", "meta.describe-function", help="Describe a registered function")
468
+ meta_function.add_argument("function_name", help="Bare registered alias; select scope with --project / --schema")
469
+ meta_function.add_argument("--project", help="Target MaxCompute project")
470
+ meta_function.add_argument("--schema", help="Schema name (overrides session default)")
471
+ meta_function.add_argument("--json", action="store_true", help="Output as JSON envelope")
472
+ meta_function.set_defaults(handler=_handle_meta_describe_function)
473
+
458
474
  meta_search = _make_parser(meta_subparsers, "search", "meta.search", help="Search tables")
459
475
  meta_search.add_argument("keyword", help="Search keyword")
460
476
  meta_search.add_argument("--schema", help="Schema name (overrides session default)")
@@ -1786,7 +1802,7 @@ def run(
1786
1802
  if getattr(exc, "logview", None):
1787
1803
  from .utils import sanitize_logview_url
1788
1804
 
1789
- stderr.write(f" LogView: {sanitize_logview_url(exc.logview)}\n")
1805
+ stderr.write(f" LogView: {sanitize_logview_url(exc.logview, include_access_token=True)}\n")
1790
1806
  return exc.exit_code
1791
1807
  except Exception as exc:
1792
1808
  error_payload = ErrorPayload(
@@ -2121,6 +2137,18 @@ def _handle_meta_list_tables(app: MaxCApp, args: argparse.Namespace, stdout: Tex
2121
2137
  _emit_envelope(envelope, args=args, stdout=stdout, default_format="table")
2122
2138
 
2123
2139
 
2140
+ def _handle_meta_list_functions(app: MaxCApp, args: argparse.Namespace, stdout: TextIO) -> None:
2141
+ envelope = app.meta_list_functions(
2142
+ project=args.project, schema=args.schema, prefix=args.prefix, limit=args.limit, cursor=args.cursor,
2143
+ )
2144
+ _emit_envelope(envelope, args=args, stdout=stdout, default_format="table")
2145
+
2146
+
2147
+ def _handle_meta_describe_function(app: MaxCApp, args: argparse.Namespace, stdout: TextIO) -> None:
2148
+ envelope = app.meta_describe_function(args.function_name, project=args.project, schema=args.schema)
2149
+ _emit_envelope(envelope, args=args, stdout=stdout, default_format="table")
2150
+
2151
+
2124
2152
  def _handle_meta_describe(app: MaxCApp, args: argparse.Namespace, stdout: TextIO) -> None:
2125
2153
  # When --json is used, always return full schema (agents need all columns)
2126
2154
  full = args.full or _is_json_mode(args)
@@ -2966,6 +2994,8 @@ def _manifest_effects(command: str) -> list[dict[str, Any]]:
2966
2994
  ),
2967
2995
  ]
2968
2996
  effects_by_command: dict[str, list[dict[str, Any]]] = {
2997
+ "meta.list-functions": [_manifest_effect("remote", "read", "maxcompute_function_catalog")],
2998
+ "meta.describe-function": [_manifest_effect("remote", "read", "maxcompute_function_metadata")],
2969
2999
  "agent.context": [
2970
3000
  _manifest_effect("local", "read", "config_and_runtime_readiness"),
2971
3001
  ],
@@ -4446,7 +4476,7 @@ def _render_human(envelope: Envelope) -> str:
4446
4476
  if logview:
4447
4477
  from .utils import sanitize_logview_url
4448
4478
 
4449
- details["logview"] = sanitize_logview_url(logview)
4479
+ details["logview"] = sanitize_logview_url(logview, include_access_token=True)
4450
4480
  if error.context:
4451
4481
  details["context"] = error.context
4452
4482
  sections.append(render_key_values(details))
@@ -4491,11 +4521,20 @@ def _render_human(envelope: Envelope) -> str:
4491
4521
  )
4492
4522
  return "\n\n".join(section for section in sections if section)
4493
4523
 
4524
+ if envelope.status == "pending" and (command == "query" or command.startswith("job.")):
4525
+ from .output import render_markdown
4526
+
4527
+ return render_markdown(envelope)
4528
+
4494
4529
  if command == "query":
4530
+ from .utils import sanitize_logview_url
4531
+
4495
4532
  rows = data.get("rows", [])
4496
4533
  summary = render_key_values(
4497
4534
  {
4498
4535
  "status": envelope.status,
4536
+ "job_id": metadata.get("job_id"),
4537
+ "logview": sanitize_logview_url(metadata.get("logview"), include_access_token=True),
4499
4538
  "project": metadata.get("project"),
4500
4539
  "elapsed_ms": metadata.get("elapsed_ms"),
4501
4540
  "returned_rows": data.get("returned_rows"),
@@ -4518,6 +4557,12 @@ def _render_human(envelope: Envelope) -> str:
4518
4557
  if command == "meta.list-tables":
4519
4558
  return render_table(data.get("tables", []))
4520
4559
 
4560
+ if command == "meta.list-functions":
4561
+ return render_table(data.get("functions", []))
4562
+
4563
+ if command == "meta.describe-function":
4564
+ return render_key_values(data.get("function", {}))
4565
+
4521
4566
  if command == "meta.describe":
4522
4567
  # Render schema/partition_columns as nested sub-tables instead of
4523
4568
  # JSON-stringifying them into a single cell.
@@ -4584,6 +4629,7 @@ _RECORD_COLLECTION_KEYS: dict[str, tuple[str, str]] = {
4584
4629
  "meta.list-projects": ("projects", "project"),
4585
4630
  "meta.list-schemas": ("schemas", "schema"),
4586
4631
  "meta.list-tables": ("tables", "table"),
4632
+ "meta.list-functions": ("functions", "function"),
4587
4633
  "meta.partitions": ("partitions", "partition"),
4588
4634
  "meta.search": ("matches", "match"),
4589
4635
  "meta.search-columns": ("matches", "match"),
@@ -4596,6 +4642,7 @@ _RECORD_DEFAULT_COLUMNS: dict[str, list[str]] = {
4596
4642
  "job.list": ["job_id", "status", "progress", "project", "submitted_at"],
4597
4643
  "meta.list-projects": ["name"],
4598
4644
  "meta.list-schemas": ["name"],
4645
+ "meta.list-functions": ["function_name", "project", "schema_name", "class_type", "owner"],
4599
4646
  "meta.list-tables": [
4600
4647
  "table_name",
4601
4648
  "schema_name",