maxc-cli 0.4.1__tar.gz → 0.4.3__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 (97) hide show
  1. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/PKG-INFO +1 -1
  2. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/setup.py +1 -1
  3. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli/__init__.py +1 -1
  4. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli/agent_platforms.py +1 -1
  5. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli/app.py +16 -5
  6. maxc_cli-0.4.3/src/maxc_cli/backend/auth.py +164 -0
  7. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli/backend/data.py +31 -0
  8. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli/cli.py +55 -9
  9. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli/helpers.py +5 -5
  10. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli/skills/SKILL.md +20 -24
  11. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli/skills/references/bootstrap-auth.md +0 -4
  12. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli/skills/references/bootstrap-flow.md +4 -13
  13. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli/skills/references/command-patterns.md +1 -1
  14. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli/skills/references/red-lines.md +3 -3
  15. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli.egg-info/PKG-INFO +1 -1
  16. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli.egg-info/SOURCES.txt +1 -1
  17. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/tests/test_agent_skill_commands_context.py +1 -1
  18. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/tests/test_integration_real.py +2 -2
  19. maxc_cli-0.4.3/tests/test_skill_eval.py +193 -0
  20. maxc_cli-0.4.1/src/maxc_cli/backend/auth.py +0 -175
  21. maxc_cli-0.4.1/src/maxc_cli/skills/references/migrate-from-odpscmd.md +0 -133
  22. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/MANIFEST.in +0 -0
  23. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/README.md +0 -0
  24. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/pyproject.toml +0 -0
  25. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/scripts/pyinstaller_entry.py +0 -0
  26. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/scripts/regression_test.py +0 -0
  27. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/setup.cfg +0 -0
  28. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli/__main__.py +0 -0
  29. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli/_samples.py +0 -0
  30. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli/audit.py +0 -0
  31. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli/auth_providers.py +0 -0
  32. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli/backend/__init__.py +0 -0
  33. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli/backend/catalog.py +0 -0
  34. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli/backend/job.py +0 -0
  35. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli/backend/meta.py +0 -0
  36. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli/backend/odps.py +0 -0
  37. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli/backend/query.py +0 -0
  38. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli/cache.py +0 -0
  39. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli/catalog_bootstrap.py +0 -0
  40. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli/config.py +0 -0
  41. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli/exceptions.py +0 -0
  42. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli/help_format.py +0 -0
  43. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli/masking.py +0 -0
  44. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli/models.py +0 -0
  45. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli/output.py +0 -0
  46. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli/setting_parser.py +0 -0
  47. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli/skills/agents/openai.yaml +0 -0
  48. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli/skills/references/json-output-format.md +0 -0
  49. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli/skills/references/maxcompute-select-guide.md +0 -0
  50. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli/skills/references/maxcompute-sql-notes.md +0 -0
  51. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli/skills/references/partition-guide.md +0 -0
  52. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli/skills/references/setup-install.md +0 -0
  53. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli/skills/references/sql-common-errors.md +0 -0
  54. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli/skills/references/sql-query-patterns.md +0 -0
  55. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli/skills/references/text2sql-principles.md +0 -0
  56. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli/store.py +0 -0
  57. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli/utils.py +0 -0
  58. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli.egg-info/dependency_links.txt +0 -0
  59. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli.egg-info/entry_points.txt +0 -0
  60. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli.egg-info/requires.txt +0 -0
  61. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/src/maxc_cli.egg-info/top_level.txt +0 -0
  62. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/tests/test_agent_hints_and_cli.py +0 -0
  63. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/tests/test_agent_platforms.py +0 -0
  64. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/tests/test_agent_skill_commands.py +0 -0
  65. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/tests/test_backend_data.py +0 -0
  66. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/tests/test_backend_data_serialization.py +0 -0
  67. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/tests/test_backend_meta.py +0 -0
  68. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/tests/test_build_release_script.py +0 -0
  69. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/tests/test_cache.py +0 -0
  70. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/tests/test_catalog.py +0 -0
  71. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/tests/test_catalog_bootstrap.py +0 -0
  72. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/tests/test_cli_arg_validation.py +0 -0
  73. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/tests/test_cli_mock.py +0 -0
  74. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/tests/test_cli_query_parse_and_sanitize.py +0 -0
  75. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/tests/test_compat.py +0 -0
  76. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/tests/test_e2e_smoke.py +0 -0
  77. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/tests/test_envelope_shape.py +0 -0
  78. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/tests/test_error_self_correction.py +0 -0
  79. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/tests/test_error_translation.py +0 -0
  80. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/tests/test_exit_codes.py +0 -0
  81. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/tests/test_external_auth.py +0 -0
  82. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/tests/test_flag_hoist.py +0 -0
  83. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/tests/test_help_format.py +0 -0
  84. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/tests/test_help_version_e2e.py +0 -0
  85. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/tests/test_helpers.py +0 -0
  86. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/tests/test_helpers_csv.py +0 -0
  87. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/tests/test_integration.py +0 -0
  88. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/tests/test_job_improvements.py +0 -0
  89. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/tests/test_masking.py +0 -0
  90. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/tests/test_meta_schema_and_partition_cols.py +0 -0
  91. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/tests/test_phase1_improvements.py +0 -0
  92. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/tests/test_pyinstaller_bundle.py +0 -0
  93. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/tests/test_query_auto_promote.py +0 -0
  94. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/tests/test_query_result_csv_fallback.py +0 -0
  95. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/tests/test_setting_parser.py +0 -0
  96. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/tests/test_skill_cli_consistency.py +0 -0
  97. {maxc_cli-0.4.1 → maxc_cli-0.4.3}/tests/test_skill_renderer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maxc-cli
3
- Version: 0.4.1
3
+ Version: 0.4.3
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
@@ -9,7 +9,7 @@ README = ROOT / "README.md"
9
9
 
10
10
  setup(
11
11
  name="maxc-cli",
12
- version="0.4.1",
12
+ version="0.4.3",
13
13
  description="Agent-native MaxCompute CLI for external coding agents",
14
14
  long_description=README.read_text(encoding="utf-8"),
15
15
  long_description_content_type="text/markdown",
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.4.1"
5
+ __version__ = "0.4.3"
@@ -118,7 +118,7 @@ def _build_registry() -> tuple[Platform, ...]:
118
118
  install_root=_claude_root(),
119
119
  skill_subpath=None,
120
120
  extra_files=(),
121
- next_step_hint="Restart Claude Code or run /reload-plugins to activate",
121
+ next_step_hint="Skill is auto-discovered — start a new Claude Code session to activate",
122
122
  ),
123
123
  Platform(name="cursor", install_root=_simple_root(".cursor"),
124
124
  next_step_hint="Restart Cursor to activate"),
@@ -607,6 +607,7 @@ class MaxCApp:
607
607
  cost_check: 'float | None' = None,
608
608
  idempotency_key: 'str | None' = None,
609
609
  force: 'bool' = False,
610
+ dry_run: 'bool' = False,
610
611
  ) -> 'Envelope':
611
612
  return self.query(
612
613
  command="job.submit",
@@ -617,6 +618,7 @@ class MaxCApp:
617
618
  cost_check=cost_check,
618
619
  idempotency_key=idempotency_key,
619
620
  force=force,
621
+ dry_run=dry_run,
620
622
  )
621
623
 
622
624
  def job_status(self, job_id: 'str') -> 'Envelope':
@@ -2465,6 +2467,7 @@ class MaxCApp:
2465
2467
  block_size: 'int' = 10000,
2466
2468
  project: 'str | None' = None,
2467
2469
  schema: 'str | None' = None,
2470
+ dry_run: 'bool' = False,
2468
2471
  ) -> 'Envelope':
2469
2472
  target_project = project or self.config.default_project
2470
2473
  result = self.backend.upload_table(
@@ -2473,6 +2476,7 @@ class MaxCApp:
2473
2476
  delimiter=delimiter, has_header=has_header,
2474
2477
  null_marker=null_marker, block_size=block_size,
2475
2478
  project=project, schema=schema,
2479
+ dry_run=dry_run,
2476
2480
  )
2477
2481
  metadata = {
2478
2482
  "project": target_project,
@@ -2480,16 +2484,21 @@ class MaxCApp:
2480
2484
  "delimiter": delimiter,
2481
2485
  "block_size": block_size,
2482
2486
  }
2487
+ if dry_run:
2488
+ actions = [action("data.upload", data=result, metadata=metadata)]
2489
+ insights = ["Dry-run validated table schema and CSV file. Re-run without --dry-run to upload."]
2490
+ else:
2491
+ actions = [action("data.sample", data=result, metadata=metadata)]
2492
+ insights = []
2483
2493
  envelope = Envelope(
2484
2494
  command="data.upload",
2485
2495
  status="success",
2486
2496
  data=result,
2487
2497
  metadata=metadata,
2488
2498
  agent_hints=AgentHints(
2489
- actions=[
2490
- action("data.sample", data=result, metadata=metadata),
2491
- ],
2499
+ actions=actions,
2492
2500
  warnings=result.get("warnings", []),
2501
+ insights=insights,
2493
2502
  ),
2494
2503
  )
2495
2504
  self.log("data.upload", envelope.status, envelope.metadata)
@@ -2931,13 +2940,15 @@ class MaxCApp:
2931
2940
  def auth_can_i(
2932
2941
  self,
2933
2942
  *,
2934
- table_name: 'str',
2943
+ object_name: 'str',
2944
+ object_type: 'str' = "Table",
2935
2945
  operation: 'str',
2936
2946
  project: 'str | None' = None,
2937
2947
  ) -> 'Envelope':
2938
2948
  target_project = project or self.config.default_project
2939
2949
  payload, warnings = self.backend.can_i_info(
2940
- table_name=table_name,
2950
+ object_name=object_name,
2951
+ object_type=object_type,
2941
2952
  operation=operation,
2942
2953
  project=target_project,
2943
2954
  )
@@ -0,0 +1,164 @@
1
+ """Auth-related mixin for OdpsBackend."""
2
+
3
+ from typing import Any
4
+ from xml.etree import ElementTree
5
+
6
+ from ..exceptions import BackendConnectionError, MaxCError
7
+ from ..helpers import (
8
+ build_odps_identity_payload,
9
+ odps_identity_source,
10
+ quote_table_name,
11
+ translate_odps_error,
12
+ )
13
+
14
+
15
+ class AuthMixin:
16
+ """Mixin providing authentication and authorization methods."""
17
+
18
+ def whoami_info(self, *, project: 'str | None' = None) -> 'tuple[dict[str, Any], list[str]]':
19
+ """Get current identity info by executing ``whoami`` security query.
20
+
21
+ Calls ``client.execute_security_query("whoami")`` to verify the
22
+ connection and return desensitized identity information.
23
+
24
+ Args:
25
+ project: Optional project override.
26
+
27
+ Returns:
28
+ Tuple of (identity payload dict, warnings list).
29
+ """
30
+ target_project = project or self.project
31
+ try:
32
+ result = self.client.execute_security_query("whoami", project=target_project)
33
+ except Exception as exc:
34
+ raise translate_odps_error(exc, "whoami") from exc
35
+
36
+ owner_display_name = result.get("DisplayName") if isinstance(result, dict) else None
37
+ if owner_display_name:
38
+ self._owner_display_name = owner_display_name
39
+ return build_odps_identity_payload(
40
+ client=self.client,
41
+ settings=self.settings,
42
+ allowed_operations=self.config.allowed_operations,
43
+ identity_source=odps_identity_source(self.setting_sources),
44
+ auth_type=getattr(self.resolved_auth, "auth_type", "access_key"),
45
+ token_expires_at=getattr(self.resolved_auth, "token_expires_at", None),
46
+ project=target_project,
47
+ owner_display_name=owner_display_name,
48
+ )
49
+
50
+ def _check_permission(
51
+ self,
52
+ *,
53
+ object_name: 'str',
54
+ object_type: 'str',
55
+ action: 'str',
56
+ project: 'str',
57
+ ) -> 'tuple[bool, str]':
58
+ """Call the MaxCompute checkPermission REST API.
59
+
60
+ Uses GET /projects/{project}/auth/?type={type}&name={name}&grantee={action}
61
+ (same endpoint as Java SDK's SecurityManager#checkPermission).
62
+
63
+ Returns:
64
+ Tuple of (allowed: bool, message: str).
65
+ """
66
+ import json as _json
67
+
68
+ rest = self.client.rest
69
+ endpoint = rest.endpoint
70
+ url = f"{endpoint}/projects/{project}/auth/"
71
+ params = {
72
+ "type": object_type,
73
+ "name": object_name,
74
+ "grantee": action,
75
+ }
76
+ resp = rest.get(url, params=params)
77
+ root = ElementTree.fromstring(resp.content)
78
+ result = (root.findtext("Result") or "").strip()
79
+ raw_message = (root.findtext("Message") or "").strip()
80
+ try:
81
+ parsed = _json.loads(raw_message)
82
+ message = parsed.get("message", "") if isinstance(parsed, dict) else raw_message
83
+ except (ValueError, TypeError):
84
+ message = raw_message
85
+ return result.upper() == "ALLOW", message
86
+
87
+ def can_i_info(
88
+ self,
89
+ *,
90
+ object_name: 'str',
91
+ object_type: 'str' = "Table",
92
+ operation: 'str',
93
+ project: 'str | None' = None,
94
+ ) -> 'tuple[dict[str, Any], list[str]]':
95
+ """Check if a specific operation is allowed on an object.
96
+
97
+ Calls the MaxCompute checkPermission REST API directly.
98
+
99
+ Args:
100
+ object_name: Object name to check (table name, function name, etc.).
101
+ object_type: Object type (Table, Project, Function, Resource, Instance).
102
+ operation: ODPS ActionType (e.g. "Select", "CreateInstance").
103
+ project: Optional project override.
104
+
105
+ Returns:
106
+ Tuple of (permission payload dict, warnings list).
107
+ """
108
+ target_project = project or self.project
109
+ if object_type == "Table":
110
+ quote_table_name(object_name)
111
+
112
+ try:
113
+ allowed, message = self._check_permission(
114
+ object_name=object_name,
115
+ object_type=object_type,
116
+ action=operation,
117
+ project=target_project,
118
+ )
119
+ except Exception as exc:
120
+ translated = translate_odps_error(exc)
121
+ if isinstance(translated, BackendConnectionError):
122
+ raise translated
123
+ return (
124
+ {
125
+ "object_type": object_type,
126
+ "object_name": object_name,
127
+ "project": target_project,
128
+ "operation": operation,
129
+ "allowed": False,
130
+ "check_mode": "odps_check_permission_api",
131
+ "reason": translated.message,
132
+ "check_error_code": translated.error_code,
133
+ },
134
+ [],
135
+ )
136
+
137
+ return (
138
+ {
139
+ "object_type": object_type,
140
+ "object_name": object_name,
141
+ "project": target_project,
142
+ "operation": operation,
143
+ "allowed": allowed,
144
+ "check_mode": "odps_check_permission_api",
145
+ "reason": message if message else ("Allowed." if allowed else "Denied."),
146
+ "check_error_code": None if allowed else "PERMISSION_DENIED",
147
+ },
148
+ [],
149
+ )
150
+
151
+ def _get_owner_display_name(self) -> 'str | None':
152
+ """Get the current user's display name (e.g., 'ALIYUN$xxx' or 'RAM$xxx')."""
153
+ if self._owner_display_name is not None:
154
+ return self._owner_display_name
155
+ try:
156
+ result = self.client.execute_security_query("whoami", project=self.project)
157
+ display_name = result.get("DisplayName") if isinstance(result, dict) else None
158
+ if display_name:
159
+ self._owner_display_name = display_name
160
+ return display_name
161
+ except Exception:
162
+ pass
163
+
164
+ return None
@@ -228,6 +228,7 @@ class DataMixin:
228
228
  block_size: 'int' = 10000,
229
229
  project: 'str | None' = None,
230
230
  schema: 'str | None' = None,
231
+ dry_run: 'bool' = False,
231
232
  ) -> 'dict[str, Any]':
232
233
  """Upload a CSV/TSV file into an existing table or partition via Tunnel.
233
234
 
@@ -295,6 +296,36 @@ class DataMixin:
295
296
  )
296
297
 
297
298
  bytes_read = os.path.getsize(file_path)
299
+
300
+ if dry_run:
301
+ warnings: list[str] = []
302
+ rows_found = 0
303
+ with open(file_path, encoding="utf-8", newline="") as fh:
304
+ reader = csv.reader(fh, delimiter=delimiter)
305
+ if has_header:
306
+ try:
307
+ header = next(reader)
308
+ except StopIteration:
309
+ header = []
310
+ column_mapping = _resolve_header_mapping(header, data_columns, warnings)
311
+ else:
312
+ column_mapping = [c.name for c in data_columns]
313
+ for _ in reader:
314
+ rows_found += 1
315
+ warnings.append("Dry-run: file and table schema validated, no data was uploaded.")
316
+ return {
317
+ "table": definition.name,
318
+ "applied_partition": partition,
319
+ "rows_written": 0,
320
+ "rows_found": rows_found,
321
+ "bytes_read": bytes_read,
322
+ "column_mapping": column_mapping,
323
+ "blocks": 0,
324
+ "overwrite": overwrite,
325
+ "dry_run": True,
326
+ "warnings": warnings,
327
+ }
328
+
298
329
  block_ids: list[int] = []
299
330
  rows_written = 0
300
331
  warnings: list[str] = []
@@ -250,6 +250,7 @@ def build_parser() -> argparse.ArgumentParser:
250
250
  job_submit.add_argument("--max-rows", type=positive_int, default=100, help="Maximum rows to return (default: 100)")
251
251
  job_submit.add_argument("--cost-check", type=float, help="Abort if estimated cost exceeds threshold (CU)")
252
252
  job_submit.add_argument("--idempotency-key", help="Deduplication key for retries")
253
+ job_submit.add_argument("--dry-run", action="store_true", help="Estimate cost without submitting")
253
254
  job_submit.add_argument("--force", action="store_true", default=False, help=argparse.SUPPRESS)
254
255
  job_submit.set_defaults(handler=_handle_job_submit)
255
256
 
@@ -453,6 +454,7 @@ def build_parser() -> argparse.ArgumentParser:
453
454
  data_upload.add_argument("--schema", help="Schema name (overrides session default)")
454
455
  data_upload.add_argument("--block-size", type=positive_int, default=10000,
455
456
  help="Rows per Tunnel block (default: 10000)")
457
+ data_upload.add_argument("--dry-run", action="store_true", help="Validate table and CSV without uploading")
456
458
  data_upload.add_argument("--project", help="Target MaxCompute project")
457
459
  data_upload.add_argument("--json", action="store_true", help="Output as JSON envelope")
458
460
  data_upload.set_defaults(handler=_handle_data_upload)
@@ -530,16 +532,57 @@ def build_parser() -> argparse.ArgumentParser:
530
532
  auth_whoami.add_argument("--json", action="store_true", help="Output as JSON envelope")
531
533
  auth_whoami.set_defaults(handler=_handle_auth_whoami)
532
534
 
533
- auth_can_i = _make_parser(auth_subparsers, "can-i", "auth.can-i", help="Check whether an operation is allowed")
534
- auth_can_i.add_argument("--table", required=True, help="Table name to check")
535
+ _CAN_I_ACTIONS = [
536
+ "Select", "Describe", "Alter", "Update", "Drop", "Download", "All",
537
+ "Read", "Write", "List",
538
+ "CreateTable", "CreateInstance", "CreateFunction", "CreateResource",
539
+ "Execute", "Delete",
540
+ ]
541
+ _CAN_I_ACTIONS_LOWER = {a.lower(): a for a in _CAN_I_ACTIONS}
542
+ _CAN_I_OBJECT_TYPES = ["Table", "Project", "Function", "Resource", "Instance"]
543
+ _CAN_I_OBJECT_TYPES_LOWER = {t.lower(): t for t in _CAN_I_OBJECT_TYPES}
544
+
545
+ auth_can_i = _make_parser(
546
+ auth_subparsers, "can-i", "auth.can-i",
547
+ help="Check whether current user has a specific permission on an object",
548
+ description=(
549
+ "Check whether the current user has a specific permission on an ODPS object.\n"
550
+ "\n"
551
+ "Examples:\n"
552
+ " maxc auth can-i --table my_table --operation Select\n"
553
+ " maxc auth can-i --table my_table --operation Update --project other_proj\n"
554
+ " maxc auth can-i --object my_proj --type Project --operation CreateTable\n"
555
+ " maxc auth can-i --object my_proj --type Project --operation CreateInstance\n"
556
+ ),
557
+ formatter_class=AliyunRawTextFormatter,
558
+ )
559
+ auth_can_i.add_argument("--object", "--table", required=True, dest="object_name",
560
+ help="Object name to check (table name, or project name when --type=Project)")
561
+ auth_can_i.add_argument(
562
+ "--type",
563
+ default="Table",
564
+ type=lambda s: _CAN_I_OBJECT_TYPES_LOWER.get(s.lower(), s),
565
+ choices=_CAN_I_OBJECT_TYPES,
566
+ help=(
567
+ "Object type (case-insensitive, default: Table). "
568
+ "Table | Project | Function | Resource | Instance."
569
+ ),
570
+ )
535
571
  auth_can_i.add_argument(
536
572
  "--operation",
537
573
  required=True,
538
- type=lambda s: s.upper(),
539
- choices=["SELECT", "INSERT", "UPDATE", "DELETE", "CREATE", "DROP", "ALTER"],
540
- help="Operation to check (only SELECT is probed against the live backend; others rely on the configured allow-list).",
574
+ type=lambda s: _CAN_I_ACTIONS_LOWER.get(s.lower(), s),
575
+ choices=_CAN_I_ACTIONS,
576
+ help=(
577
+ "Permission to check (case-insensitive). "
578
+ "Table: Select, Describe, Alter, Update, Drop, Download, All. "
579
+ "Project: CreateTable, CreateInstance, CreateFunction, CreateResource, List, Read, Write, All. "
580
+ "Function: Read, Write, Delete, Execute, All. "
581
+ "Resource: Read, Write, Delete, All. "
582
+ "Instance: Read, Write, All."
583
+ ),
541
584
  )
542
- auth_can_i.add_argument("--project", help="Target MaxCompute project")
585
+ auth_can_i.add_argument("--project", help="Project where the object lives (default: current project)")
543
586
  auth_can_i.add_argument("--json", action="store_true", help="Output as JSON envelope")
544
587
  auth_can_i.set_defaults(handler=_handle_auth_can_i)
545
588
 
@@ -800,13 +843,13 @@ def _is_json_mode(args: argparse.Namespace) -> bool:
800
843
 
801
844
 
802
845
  def _build_permission_denied_hints(app: MaxCApp | None) -> AgentHints:
803
- """Build PERMISSION_DENIED agent hints, suggesting _dev workspace switch when appropriate."""
846
+ """Build PERMISSION_DENIED agent hints, suggesting _dev project switch when appropriate."""
804
847
  actions = []
805
848
  project = app.config.default_project if app else None
806
849
  if project and not project.endswith("_dev"):
807
850
  actions.append(SuggestedAction(
808
851
  id="session.set",
809
- title="Switch to dev workspace",
852
+ title="Switch to dev project",
810
853
  command=f"maxc session set --project {project}_dev --json",
811
854
  ))
812
855
  actions.append(action("query", metadata={"sql_executed": "SELECT 1"}))
@@ -1100,6 +1143,7 @@ def _handle_job_submit(app: MaxCApp, args: argparse.Namespace, stdout: TextIO) -
1100
1143
  cost_check=args.cost_check,
1101
1144
  idempotency_key=args.idempotency_key,
1102
1145
  force=args.force,
1146
+ dry_run=args.dry_run,
1103
1147
  )
1104
1148
  _emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
1105
1149
 
@@ -1372,6 +1416,7 @@ def _handle_data_upload(app: MaxCApp, args: argparse.Namespace, stdout: TextIO)
1372
1416
  block_size=args.block_size,
1373
1417
  project=args.project,
1374
1418
  schema=getattr(args, "schema", None),
1419
+ dry_run=args.dry_run,
1375
1420
  )
1376
1421
  _emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
1377
1422
 
@@ -1450,7 +1495,8 @@ def _handle_auth_whoami(app: MaxCApp, args: argparse.Namespace, stdout: TextIO)
1450
1495
 
1451
1496
  def _handle_auth_can_i(app: MaxCApp, args: argparse.Namespace, stdout: TextIO) -> None:
1452
1497
  envelope = app.auth_can_i(
1453
- table_name=args.table,
1498
+ object_name=args.object_name,
1499
+ object_type=args.type,
1454
1500
  operation=args.operation,
1455
1501
  project=args.project,
1456
1502
  )
@@ -1039,12 +1039,12 @@ def classify_sql_error(message: str) -> dict[str, Any]:
1039
1039
  return {"error_type": "unknown"}
1040
1040
 
1041
1041
 
1042
- def _dev_workspace_hint(project: str | None) -> str:
1043
- """Return a hint about switching to the _dev workspace if the project is not a dev workspace."""
1042
+ def _dev_project_hint(project: str | None) -> str:
1043
+ """Return a hint about switching to the _dev project if the current project is production."""
1044
1044
  if project and not project.endswith("_dev"):
1045
1045
  return (
1046
- f"Current project '{project}' is a production workspace. "
1047
- f"Personal accounts usually only have access to the dev workspace (_dev). "
1046
+ f"Current project '{project}' is a production project. "
1047
+ f"Personal accounts usually only have access to the dev project (_dev). "
1048
1048
  f"Try switching: maxc session set --project {project}_dev"
1049
1049
  )
1050
1050
  return ""
@@ -1063,7 +1063,7 @@ def _build_permission_error(
1063
1063
  preserved as the error message for diagnostics. Context-specific
1064
1064
  guidance goes into the suggestion field.
1065
1065
  """
1066
- dev_hint = _dev_workspace_hint(project_name)
1066
+ dev_hint = _dev_project_hint(project_name)
1067
1067
 
1068
1068
  if context == "list_projects" and project_name:
1069
1069
  suggestion = f"Verify that your account has `odps:Read` on project {project_name}, or contact the project owner."
@@ -11,7 +11,6 @@ Use the live CLI instead of inventing a separate MaxCompute adapter. Prefer `{{c
11
11
 
12
12
  - First-time setup or repair of Python or `maxc-cli`
13
13
  - Auth bootstrap or identity inspection (AK/SK or env vars)
14
- - Migrating from odpscmd (reusing existing ODPS Console credentials)
15
14
  - Session project or schema overrides
16
15
  - Metadata discovery, schema inspection, fast table/column search
17
16
  - Read-only query execution or job tracking
@@ -43,9 +42,9 @@ These are non-negotiable. See [references/red-lines.md](references/red-lines.md)
43
42
 
44
43
  1. **Always use `--json`** for machine work. Use `--format markdown` for user-facing output, `--format brief` in token-tight contexts. `--json` is shorthand for `--format json`. **`--format` is a top-level flag — it must come before the subcommand**: `{{cli}} --format markdown query "SELECT 1"` (✓), not `{{cli}} query "SELECT 1" --format markdown` (✗). `--json` may appear anywhere because each subcommand also accepts it.
45
44
  2. **Never invent names** — table, schema, project, or endpoint. Verify with `meta` commands and `auth whoami`.
46
- 3. **Default to `--project` for the user's target workspace.** The configured project (in `~/.maxc/config.yaml`) is the user's **home dev workspace** — the data they actually want to query usually lives in a *different* workspace (often the corresponding production one). When the user mentions a table/project without specifying which workspace, **ask first**, then pass `--project <name>` on every meta/data command and use `project.table` in SQL.
47
- 4. **Workspace naming convention is a fixed pair:** `<name>_dev` is the dev workspace; the same `<name>` **without** the `_dev` suffix is its corresponding **production** workspace. They share metadata structure but hold different data and different permissions. See Dev vs Production Workspaces below.
48
- 5. **Never re-prompt for credentials** when `auth whoami` shows `authenticated=true`. Permission errors are almost always a workspace/project issue, not a credential issue.
45
+ 3. **Default to `--project` for the user's target project.** The configured project (in `~/.maxc/config.yaml`) is the user's **dev project** — the data they actually want to query usually lives in a *different* project (often the corresponding production one). When the user mentions a table/project without specifying which environment, **ask first**, then pass `--project <name>` on every meta/data command and use `project.table` in SQL.
46
+ 4. **Project naming convention is a fixed pair:** `<name>_dev` is the dev project; the same `<name>` **without** the `_dev` suffix is its corresponding **production** project. Together they form one DataWorks workspace. They share metadata structure but hold different data and different permissions. See Dev vs Production Projects below.
47
+ 5. **Never re-prompt for credentials** when `auth whoami` shows `authenticated=true`. Permission errors are almost always a project environment issue (dev vs prod), not a credential issue.
49
48
  6. **Always discover partitions** via `meta latest-partition` before querying partitioned tables. Format varies per table.
50
49
  7. **Always read `error.suggestion`** before retrying a failed command. Same input → same error.
51
50
  8. **Never install or upgrade Python** without explicit user confirmation.
@@ -56,8 +55,7 @@ These are non-negotiable. See [references/red-lines.md](references/red-lines.md)
56
55
  When `auth whoami --json` returns `configured=false` (no auth set up), follow [references/bootstrap-flow.md](references/bootstrap-flow.md) step by step. Three principles:
57
56
 
58
57
  1. **Never pick the auth method yourself** — always ask the user to choose between AK/SK and environment variables.
59
- 2. **If the user already has `odpscmd` configured**, proactively offer to migrate those credentials before asking them to enter anything new see [references/migrate-from-odpscmd.md](references/migrate-from-odpscmd.md).
60
- 3. **If `auth whoami` shows `auth_type=external`, the user is on an externally-managed credential provider — do NOT modify the auth config.** Treat the bootstrap as already done. Only `project`/`endpoint`/`schema` are safe to change (via `session set` or by re-running `auth login-external` with the same `--process-command`).
58
+ 2. **If `auth whoami` shows `auth_type=external`, the user is on an externally-managed credential provider — do NOT modify the auth config.** Treat the bootstrap as already done. Only `project`/`endpoint`/`schema` are safe to change (via `session set` or by re-running `auth login-external` with the same `--process-command`).
61
59
 
62
60
  ## First Pass
63
61
 
@@ -87,38 +85,38 @@ Key paths:
87
85
 
88
86
  See [references/json-output-format.md](references/json-output-format.md) for full examples and [references/command-patterns.md](references/command-patterns.md) §JSON Contract for all data shapes.
89
87
 
90
- ## Dev vs Production Workspaces
88
+ ## Dev vs Production Projects
91
89
 
92
- MaxCompute workspaces come in **fixed pairs**: a dev workspace and its corresponding production workspace. Confusing the two is the #1 source of permission errors.
90
+ A single **DataWorks workspace** corresponds to **two MaxCompute projects**: a dev project and a production project. Confusing the two is the #1 source of permission errors.
93
91
 
94
92
  ### The naming pair (memorize this)
95
93
 
96
- | Workspace type | Name pattern | Example | Who can access | What lives there |
97
- |----------------|--------------|---------|----------------|------------------|
94
+ | Project type | Name pattern | Example | Who can access | What lives there |
95
+ |----------------|--------------|------------------|----------------|------------------|
98
96
  | **Dev** | `<name>_dev` | `my_project_dev` | Personal accounts (the user themselves) | Test data, scratch tables, the user's own work |
99
97
  | **Production** | `<name>` | `my_project` | Usually only service accounts / DataWorks pipelines | The real business data the user actually wants to query |
100
98
 
101
- The dev workspace and the production workspace **share metadata structure but hold different data**. A table that exists in `my_project_dev` almost always exists in `my_project` too — but the rows, partitions, and freshness will differ.
99
+ Both projects belong to the same DataWorks workspace (`my_project`). They **share metadata structure but hold different data**. A table that exists in `my_project_dev` almost always exists in `my_project` too — but the rows, partitions, and freshness will differ.
102
100
 
103
101
  ### Other key facts
104
102
 
105
- - The project configured in `~/.maxc/config.yaml` or env vars is always the **dev** workspace — this is the user's home workspace.
106
- - Personal accounts usually only have *write* access to dev and *read* access to production (varies by org policy). Pointing a session directly at production often results in `PERMISSION_DENIED`.
107
- - `--project` is the canonical way to access **another project's** tables — most often the corresponding production workspace, occasionally a different team's project.
108
- - When the user asks about a table without naming the workspace, **ask whether they mean the dev or production copy** before guessing.
103
+ - The project configured in `~/.maxc/config.yaml` or env vars is always the **dev project** — this is the user's home project.
104
+ - Personal accounts usually only have *write* access to dev and *read* access to production (varies by org policy). Pointing a session directly at the production project often results in `PERMISSION_DENIED`.
105
+ - `--project` is the canonical way to access **another project's** tables — most often the corresponding production project, occasionally a different team's project.
106
+ - When the user asks about a table without naming the project, **ask whether they mean the dev or production copy** before guessing.
109
107
 
110
- ### How to tell which workspace you are in
108
+ ### How to tell which project you are in
111
109
 
112
110
  ```bash
113
111
  {{cli}} auth whoami --json # check data.identity.project — ends with _dev?
114
112
  {{cli}} session show --json # check current session project
115
113
  ```
116
114
 
117
- If the project name does NOT end with `_dev`, you may be pointed at a production workspace by mistake.
115
+ If the project name does NOT end with `_dev`, you may be pointed at the production project by mistake.
118
116
 
119
- ### Accessing production tables from dev workspace
117
+ ### Accessing production tables from dev project
120
118
 
121
- Use `--project` to read metadata from the production workspace without switching session:
119
+ Use `--project` to read metadata from the production project without switching session:
122
120
 
123
121
  ```bash
124
122
  {{cli}} meta list-tables --project my_project --json
@@ -126,10 +124,10 @@ Use `--project` to read metadata from the production workspace without switching
126
124
  {{cli}} data sample my_table --project my_project --json
127
125
  ```
128
126
 
129
- When writing SQL, use `project.table` format to reference tables in another workspace:
127
+ When writing SQL, use `project.table` format to reference tables in another project:
130
128
 
131
129
  ```sql
132
- -- From dev workspace, query a production table
130
+ -- From dev project, query a production table
133
131
  SELECT * FROM my_project.my_table WHERE ds = '20260418' LIMIT 100
134
132
  ```
135
133
 
@@ -139,7 +137,7 @@ Do NOT use bare table names (`FROM my_table`) when the target table lives in a d
139
137
 
140
138
  | Scenario | Symptom | Fix |
141
139
  |----------|---------|-----|
142
- | Config points to production workspace | `PERMISSION_DENIED` on most operations | `{{cli}} session set --project my_project_dev` |
140
+ | Config points to production project | `PERMISSION_DENIED` on most operations | `{{cli}} session set --project my_project_dev` |
143
141
  | Need to read production table metadata | `PERMISSION_DENIED` on `meta describe` | `{{cli}} meta describe my_table --project my_project --json` |
144
142
  | SQL references a table in another project without project prefix | `TABLE_NOT_FOUND` | Use `project.table` format in SQL |
145
143
  | Mixed access: dev metadata + production data | Confusing results | Be explicit: use `--project` for metadata commands, `project.table` in SQL |
@@ -227,7 +225,6 @@ For full command syntax and options, see [references/command-patterns.md](refere
227
225
  | Check permission for an op | `{{cli}} auth can-i --table T --operation SELECT --json` |
228
226
  | Diagnose a failed job | `{{cli}} job diagnose <id> --json` |
229
227
  | Add semantic metadata to a table | `{{cli}} meta semantic set T ... --json` (see command-patterns.md §Semantic Metadata) |
230
- | Migrate from odpscmd | See [references/migrate-from-odpscmd.md](references/migrate-from-odpscmd.md) |
231
228
  | Plan NL→SQL before writing | See [references/text2sql-principles.md](references/text2sql-principles.md) |
232
229
  | Look up MaxCompute SQL dialect rule | See [references/maxcompute-select-guide.md](references/maxcompute-select-guide.md) |
233
230
  | Pick a query template (Top-N, PIVOT, retention, …) | See [references/sql-query-patterns.md](references/sql-query-patterns.md) |
@@ -260,7 +257,6 @@ For full command syntax and options, see [references/command-patterns.md](refere
260
257
  | [bootstrap-flow.md](references/bootstrap-flow.md) | First-time setup or `configured=false` |
261
258
  | [setup-install.md](references/setup-install.md) | Python / maxc-cli install detail |
262
259
  | [bootstrap-auth.md](references/bootstrap-auth.md) | Per-method auth setup (AK/SK, env vars) |
263
- | [migrate-from-odpscmd.md](references/migrate-from-odpscmd.md) | User has `odpscmd` configured |
264
260
  | [command-patterns.md](references/command-patterns.md) | Full command syntax, output shapes, semantic, multi-project, schema, async |
265
261
  | [json-output-format.md](references/json-output-format.md) | JSON envelope examples |
266
262
  | [partition-guide.md](references/partition-guide.md) | Partition naming, MAX_PT() guidance, ambiguity |
@@ -28,10 +28,6 @@ If the console script is not on `PATH`, use:
28
28
 
29
29
  In sandboxes or CI, make sure those paths are writable.
30
30
 
31
- ### Migrating From odpscmd
32
-
33
- If the user already has `odpscmd` configured, reuse their credentials — see [migrate-from-odpscmd.md](migrate-from-odpscmd.md) for the field mapping and config conversion steps.
34
-
35
31
  ---
36
32
 
37
33
  ## Step 1: Check Current Auth Status
@@ -7,7 +7,7 @@ When `auth whoami --json` returns `configured=false`, follow the three phases be
7
7
  ```
8
8
  Phase 1: Prerequisites → setup-install.md
9
9
 
10
- Phase 2: Auth → bootstrap-auth.md (or migrate-from-odpscmd.md)
10
+ Phase 2: Auth → bootstrap-auth.md
11
11
 
12
12
  Phase 3: Verify → {{cli}} auth whoami --json
13
13
  ```
@@ -51,25 +51,17 @@ Then jump to the matching path in [bootstrap-auth.md](bootstrap-auth.md):
51
51
 
52
52
  If `auth whoami --json` shows `auth_type=external` (or `provider: external` in the saved config), the user is on an externally-managed credential provider. **Do not run Phase 2.** The auth is already set up — only `project`/`endpoint`/`schema` are safe to change via `session set` or by re-running the original `auth login-external` with updated `--project`/`--endpoint`. Treat bootstrap as complete and move to Phase 3.
53
53
 
54
- ### Shortcut for users with `odpscmd` already configured
55
-
56
- Before asking the question above, check whether the user already has `odpscmd` set up. If yes:
57
-
58
- > "Do you already have `odpscmd` configured? We can migrate the existing credentials without you re-entering anything."
59
-
60
- Then follow [migrate-from-odpscmd.md](migrate-from-odpscmd.md).
61
-
62
54
  ### Always confirm project and endpoint
63
55
 
64
56
  Regardless of method, ask the user explicitly for `project` and `endpoint`. If a value is already in the config or env, present it as a default but require confirmation. See [bootstrap-auth.md](bootstrap-auth.md) §"Always ask for project and endpoint".
65
57
 
66
- ### Dev vs production workspace check
58
+ ### Dev vs production project check
67
59
 
68
60
  If the project name does **not** end with `_dev`, warn the user:
69
61
 
70
- > "Project `<project>` does not end with `_dev`. Personal accounts usually only have access to dev workspaces — would you like to switch to `<project>_dev`?"
62
+ > "Project `<project>` does not end with `_dev`. Personal accounts usually only have access to dev projects — would you like to switch to `<project>_dev`?"
71
63
 
72
- See SKILL.md §"Dev vs Production Workspaces" for the full rationale.
64
+ See SKILL.md §"Dev vs Production Projects" for the full rationale.
73
65
 
74
66
  ---
75
67
 
@@ -100,5 +92,4 @@ Expected: `data.identity.authenticated=true`. `validation_status` interpretation
100
92
 
101
93
  - Step-by-step Python / maxc-cli installation — see [setup-install.md](setup-install.md).
102
94
  - Each auth method's exact CLI flags and saved YAML — see [bootstrap-auth.md](bootstrap-auth.md).
103
- - odpscmd field-by-field migration — see [migrate-from-odpscmd.md](migrate-from-odpscmd.md).
104
95
  - Public cloud endpoint catalog — present in [bootstrap-auth.md](bootstrap-auth.md) Path A.