maxc-cli 0.3.1__tar.gz → 0.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/PKG-INFO +1 -1
  2. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/setup.py +1 -1
  3. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/__init__.py +1 -1
  4. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/app.py +1 -1
  5. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/backend/query.py +49 -12
  6. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli.egg-info/PKG-INFO +1 -1
  7. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_external_auth.py +54 -0
  8. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_phase1_improvements.py +5 -5
  9. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_setting_parser.py +40 -4
  10. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/MANIFEST.in +0 -0
  11. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/README.md +0 -0
  12. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/pyproject.toml +0 -0
  13. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/scripts/pyinstaller_entry.py +0 -0
  14. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/scripts/regression_test.py +0 -0
  15. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/setup.cfg +0 -0
  16. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/__main__.py +0 -0
  17. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/_samples.py +0 -0
  18. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/agent_platforms.py +0 -0
  19. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/audit.py +0 -0
  20. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/auth_providers.py +0 -0
  21. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/backend/__init__.py +0 -0
  22. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/backend/auth.py +0 -0
  23. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/backend/catalog.py +0 -0
  24. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/backend/data.py +0 -0
  25. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/backend/job.py +0 -0
  26. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/backend/meta.py +0 -0
  27. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/backend/odps.py +0 -0
  28. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/cache.py +0 -0
  29. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/catalog_bootstrap.py +0 -0
  30. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/cli.py +0 -0
  31. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/config.py +0 -0
  32. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/exceptions.py +0 -0
  33. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/help_format.py +0 -0
  34. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/helpers.py +0 -0
  35. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/masking.py +0 -0
  36. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/models.py +0 -0
  37. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/output.py +0 -0
  38. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/setting_parser.py +0 -0
  39. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/skills/SKILL.md +0 -0
  40. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/skills/agents/openai.yaml +0 -0
  41. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/skills/references/bootstrap-auth.md +0 -0
  42. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/skills/references/bootstrap-flow.md +0 -0
  43. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/skills/references/command-patterns.md +0 -0
  44. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/skills/references/json-output-format.md +0 -0
  45. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/skills/references/maxcompute-select-guide.md +0 -0
  46. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/skills/references/maxcompute-sql-notes.md +0 -0
  47. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/skills/references/migrate-from-odpscmd.md +0 -0
  48. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/skills/references/partition-guide.md +0 -0
  49. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/skills/references/red-lines.md +0 -0
  50. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/skills/references/setup-install.md +0 -0
  51. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/skills/references/sql-common-errors.md +0 -0
  52. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/skills/references/sql-query-patterns.md +0 -0
  53. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/skills/references/text2sql-principles.md +0 -0
  54. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/store.py +0 -0
  55. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/utils.py +0 -0
  56. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli.egg-info/SOURCES.txt +0 -0
  57. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli.egg-info/dependency_links.txt +0 -0
  58. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli.egg-info/entry_points.txt +0 -0
  59. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli.egg-info/requires.txt +0 -0
  60. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli.egg-info/top_level.txt +0 -0
  61. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_agent_hints_and_cli.py +0 -0
  62. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_agent_platforms.py +0 -0
  63. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_agent_skill_commands.py +0 -0
  64. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_agent_skill_commands_context.py +0 -0
  65. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_backend_data.py +0 -0
  66. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_backend_data_serialization.py +0 -0
  67. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_backend_meta.py +0 -0
  68. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_build_release_script.py +0 -0
  69. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_cache.py +0 -0
  70. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_catalog.py +0 -0
  71. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_catalog_bootstrap.py +0 -0
  72. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_cli_arg_validation.py +0 -0
  73. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_cli_mock.py +0 -0
  74. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_cli_query_parse_and_sanitize.py +0 -0
  75. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_compat.py +0 -0
  76. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_e2e_smoke.py +0 -0
  77. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_envelope_shape.py +0 -0
  78. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_error_self_correction.py +0 -0
  79. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_error_translation.py +0 -0
  80. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_exit_codes.py +0 -0
  81. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_flag_hoist.py +0 -0
  82. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_help_format.py +0 -0
  83. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_helpers.py +0 -0
  84. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_helpers_csv.py +0 -0
  85. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_integration.py +0 -0
  86. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_integration_real.py +0 -0
  87. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_job_improvements.py +0 -0
  88. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_masking.py +0 -0
  89. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_meta_schema_and_partition_cols.py +0 -0
  90. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_pyinstaller_bundle.py +0 -0
  91. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_query_auto_promote.py +0 -0
  92. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_query_result_csv_fallback.py +0 -0
  93. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_skill_cli_consistency.py +0 -0
  94. {maxc_cli-0.3.1 → maxc_cli-0.3.2}/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.3.1
3
+ Version: 0.3.2
4
4
  Summary: Agent-native MaxCompute CLI for external coding agents
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: Programming Language :: Python :: 3.9
@@ -9,7 +9,7 @@ README = ROOT / "README.md"
9
9
 
10
10
  setup(
11
11
  name="maxc-cli",
12
- version="0.3.1",
12
+ version="0.3.2",
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.3.1"
5
+ __version__ = "0.3.2"
@@ -21,6 +21,7 @@ from .backend import OdpsBackend
21
21
  from .cache import LocalCache
22
22
  from .config import (
23
23
  AuthConfig,
24
+ ExternalAuthConfig,
24
25
  TableDefinition,
25
26
  default_global_config_path,
26
27
  load_config,
@@ -2740,7 +2741,6 @@ class MaxCApp:
2740
2741
  JSON to stdout. See :class:`ExternalCredentialProvider` for the
2741
2742
  expected JSON format.
2742
2743
  """
2743
- from .auth_providers import ExternalAuthConfig
2744
2744
  target_path = target_config_path or default_global_config_path()
2745
2745
  existing_payload = load_config_mapping(target_path) if target_path.exists() else {}
2746
2746
  existing_auth = AuthConfig.from_mapping(existing_payload.get("auth", {}) or {})
@@ -46,15 +46,20 @@ def _count_statements(sql: 'str') -> 'int':
46
46
  return sum(1 for part in cleaned.split(";") if part.strip())
47
47
 
48
48
 
49
- def _parse_sql_with_hints(sql: 'str', *, force: 'bool' = False) -> 'tuple[str, dict[str, str]]':
49
+ def _parse_sql_with_hints(
50
+ sql: 'str', *, force: 'bool' = False,
51
+ ) -> 'tuple[str, dict[str, str], int | None]':
50
52
  """Extract SET statements from *sql* and enforce client-side read-only mode.
51
53
 
52
- Returns ``(remaining_sql, merged_hints)`` where ``merged_hints``
53
- contains only user-supplied SET values. Write operations
54
- (INSERT, CREATE, DROP, etc.) are blocked unless *force* is ``True``.
54
+ Returns ``(remaining_sql, merged_hints, priority)``. ``merged_hints``
55
+ contains user-supplied SET values minus ``odps.instance.priority``,
56
+ which is lifted out into ``priority`` so callers can pass it as the
57
+ ``priority=`` kwarg of ``run_sql`` / ``execute_sql``.
55
58
 
56
- Empty SQL raises ``ValidationError``. Multi-statement SQL automatically
57
- receives ``odps.sql.submit.mode=script`` unless the user already set it.
59
+ Write operations (INSERT, CREATE, DROP, etc.) are blocked unless
60
+ *force* is ``True``. Empty SQL raises ``ValidationError``.
61
+ Multi-statement SQL automatically receives
62
+ ``odps.sql.submit.mode=script`` unless the user already set it.
58
63
  """
59
64
  parsed = SettingParser.parse(sql)
60
65
  if parsed.errors:
@@ -87,7 +92,34 @@ def _parse_sql_with_hints(sql: 'str', *, force: 'bool' = False) -> 'tuple[str, d
87
92
  if _count_statements(remaining) >= 2:
88
93
  hints.setdefault("odps.sql.submit.mode", "script")
89
94
 
90
- return remaining, hints
95
+ # odps.instance.priority is not a SQL hint — it's a top-level kwarg on
96
+ # run_sql/execute_sql. Lift it out so the caller can thread it through.
97
+ priority = _pop_priority(hints)
98
+
99
+ return remaining, hints, priority
100
+
101
+
102
+ def _pop_priority(hints: 'dict[str, str]') -> 'int | None':
103
+ """Pop ``odps.instance.priority`` from *hints* and parse as int.
104
+
105
+ Match is case-insensitive on the key. Returns ``None`` if absent.
106
+ Raises ``ValidationError`` if the value isn't an integer.
107
+ """
108
+ matched_key: str | None = None
109
+ for k in hints:
110
+ if k.lower() == "odps.instance.priority":
111
+ matched_key = k
112
+ break
113
+ if matched_key is None:
114
+ return None
115
+ raw = hints.pop(matched_key)
116
+ try:
117
+ return int(raw)
118
+ except (TypeError, ValueError):
119
+ raise ValidationError(
120
+ f"Invalid odps.instance.priority value {raw!r}: must be an integer.",
121
+ suggestion="Use SET odps.instance.priority=N; where N is an integer.",
122
+ ) from None
91
123
 
92
124
 
93
125
  class QueryMixin:
@@ -125,7 +157,8 @@ class QueryMixin:
125
157
  ValidationError: If SET syntax is invalid.
126
158
  BackendConnectionError: If ODPS connection fails.
127
159
  """
128
- actual_sql, hints = _parse_sql_with_hints(sql, force=force)
160
+ actual_sql, hints, priority = _parse_sql_with_hints(sql, force=force)
161
+ priority_kwargs = {"priority": priority} if priority is not None else {}
129
162
 
130
163
  started_at = now_utc_iso()
131
164
  started_monotonic = monotonic()
@@ -162,7 +195,7 @@ class QueryMixin:
162
195
 
163
196
  try:
164
197
  instance = self.client.run_sql(
165
- actual_sql, project=project, hints=hints,
198
+ actual_sql, project=project, hints=hints, **priority_kwargs,
166
199
  )
167
200
  except Exception as exc:
168
201
  raise translate_odps_error(exc) from exc
@@ -204,7 +237,7 @@ class QueryMixin:
204
237
  Returns:
205
238
  Dict with estimated_input_size_bytes, sql_complexity, sql_udf_num, etc.
206
239
  """
207
- actual_sql, hints = _parse_sql_with_hints(sql, force=force)
240
+ actual_sql, hints, _priority = _parse_sql_with_hints(sql, force=force)
208
241
  started_monotonic = monotonic()
209
242
  try:
210
243
  sql_cost = self.client.execute_sql_cost(
@@ -240,7 +273,8 @@ class QueryMixin:
240
273
  Returns:
241
274
  Dict with query outline, cost metadata, and ``execution_plan`` text.
242
275
  """
243
- actual_sql, hints = _parse_sql_with_hints(sql, force=force)
276
+ actual_sql, hints, priority = _parse_sql_with_hints(sql, force=force)
277
+ priority_kwargs = {"priority": priority} if priority is not None else {}
244
278
  # script-mode auto-hint doesn't apply to EXPLAIN itself; remove if present.
245
279
  explain_hints = {k: v for k, v in hints.items() if k != "odps.sql.submit.mode"}
246
280
  started_monotonic = monotonic()
@@ -252,6 +286,7 @@ class QueryMixin:
252
286
  f"EXPLAIN {actual_sql}",
253
287
  project=project,
254
288
  hints=explain_hints,
289
+ **priority_kwargs,
255
290
  )
256
291
  try:
257
292
  results = instance.get_task_results()
@@ -319,7 +354,8 @@ class QueryMixin:
319
354
  """
320
355
  from ..models import JobInfo
321
356
 
322
- actual_sql, hints = _parse_sql_with_hints(sql, force=force)
357
+ actual_sql, hints, priority = _parse_sql_with_hints(sql, force=force)
358
+ priority_kwargs = {"priority": priority} if priority is not None else {}
323
359
 
324
360
  try:
325
361
  instance = self.client.run_sql(
@@ -327,6 +363,7 @@ class QueryMixin:
327
363
  project=project,
328
364
  hints=hints,
329
365
  unique_identifier_id=idempotency_key,
366
+ **priority_kwargs,
330
367
  )
331
368
  except Exception as exc:
332
369
  raise translate_odps_error(exc) from exc
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maxc-cli
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: Agent-native MaxCompute CLI for external coding agents
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: Programming Language :: Python :: 3.9
@@ -790,3 +790,57 @@ class TestAuthSeemsConfiguredExternal:
790
790
  self._clear_odps_env(monkeypatch)
791
791
  config = _minimal_config()
792
792
  assert _auth_seems_configured(self._stub_app(config)) is False
793
+
794
+
795
+ # ============================================================
796
+ # MaxCApp.auth_login_external — end-to-end regression
797
+ # ============================================================
798
+
799
+ class TestAuthLoginExternalAppMethod:
800
+ """Regression: a stale lazy import (``from .auth_providers import
801
+ ExternalAuthConfig``) in ``MaxCApp.auth_login_external`` raised
802
+ ``ImportError`` at runtime after a ruff F401 sweep removed the
803
+ transitive re-export from ``auth_providers``. The dataclass lives in
804
+ ``config``; the import must come from there. This test exercises the
805
+ method end-to-end so any future re-routing of ``ExternalAuthConfig``
806
+ that breaks the call site is caught immediately.
807
+ """
808
+
809
+ @staticmethod
810
+ def _clear_odps_env(monkeypatch):
811
+ import maxc_cli.backend as backend_module
812
+ for aliases in backend_module.ODPS_ENV_ALIASES.values():
813
+ for alias in aliases:
814
+ monkeypatch.delenv(alias, raising=False)
815
+
816
+ def test_writes_external_provider_config_without_validation(self, tmp_path, monkeypatch):
817
+ import yaml
818
+
819
+ from maxc_cli.app import MaxCApp
820
+ self._clear_odps_env(monkeypatch)
821
+ monkeypatch.chdir(tmp_path)
822
+ monkeypatch.setenv("HOME", str(tmp_path))
823
+
824
+ config_path = tmp_path / "config.yaml"
825
+ app = MaxCApp(cwd=tmp_path, load_backend=False)
826
+
827
+ envelope = app.auth_login_external(
828
+ process_command="/usr/bin/echo '{}'",
829
+ process_timeout=30,
830
+ project="proj_x",
831
+ endpoint="http://service.cn-test.maxcompute.aliyun.com/api",
832
+ region_name="cn-test",
833
+ no_validate=True,
834
+ target_config_path=config_path,
835
+ )
836
+
837
+ assert envelope.status == "success"
838
+ assert envelope.command == "auth.login-external"
839
+ assert envelope.data["auth_type"] == "external"
840
+ assert envelope.data["process_command"] == "/usr/bin/echo '{}'"
841
+
842
+ saved = yaml.safe_load(config_path.read_text(encoding="utf-8"))
843
+ assert saved["auth"]["provider"] == "external"
844
+ assert saved["auth"]["external"]["process_command"] == "/usr/bin/echo '{}'"
845
+ assert saved["auth"]["external"]["process_timeout"] == 30
846
+ assert saved["auth"]["project"] == "proj_x"
@@ -572,19 +572,19 @@ class TestParseSqlWithHints:
572
572
  def test_multi_statement_injects_script_mode(self):
573
573
  from maxc_cli.backend.query import _parse_sql_with_hints
574
574
 
575
- _, hints = _parse_sql_with_hints("SELECT 1; SELECT 2")
575
+ _, hints, _ = _parse_sql_with_hints("SELECT 1; SELECT 2")
576
576
  assert hints.get("odps.sql.submit.mode") == "script"
577
577
 
578
578
  def test_single_statement_does_not_inject_script_mode(self):
579
579
  from maxc_cli.backend.query import _parse_sql_with_hints
580
580
 
581
- _, hints = _parse_sql_with_hints("SELECT 1")
581
+ _, hints, _ = _parse_sql_with_hints("SELECT 1")
582
582
  assert "odps.sql.submit.mode" not in hints
583
583
 
584
584
  def test_user_provided_script_mode_is_preserved(self):
585
585
  from maxc_cli.backend.query import _parse_sql_with_hints
586
586
 
587
- _, hints = _parse_sql_with_hints(
587
+ _, hints, _ = _parse_sql_with_hints(
588
588
  "SET odps.sql.submit.mode=non_script; SELECT 1; SELECT 2"
589
589
  )
590
590
  # User's value wins
@@ -593,13 +593,13 @@ class TestParseSqlWithHints:
593
593
  def test_trailing_semicolon_is_not_treated_as_multistatement(self):
594
594
  from maxc_cli.backend.query import _parse_sql_with_hints
595
595
 
596
- _, hints = _parse_sql_with_hints("SELECT 1;")
596
+ _, hints, _ = _parse_sql_with_hints("SELECT 1;")
597
597
  assert "odps.sql.submit.mode" not in hints
598
598
 
599
599
  def test_comments_are_not_counted_as_statements(self):
600
600
  from maxc_cli.backend.query import _parse_sql_with_hints
601
601
 
602
- _, hints = _parse_sql_with_hints(
602
+ _, hints, _ = _parse_sql_with_hints(
603
603
  "-- header;\nSELECT 1; -- trailing comment;"
604
604
  )
605
605
  assert "odps.sql.submit.mode" not in hints
@@ -91,19 +91,21 @@ def test_set_with_escaped_semicolon():
91
91
 
92
92
 
93
93
  def test_parse_sql_with_hints_default_no_extra_hints():
94
- actual_sql, hints = _parse_sql_with_hints("SELECT 1")
94
+ actual_sql, hints, priority = _parse_sql_with_hints("SELECT 1")
95
95
  assert actual_sql == "SELECT 1"
96
96
  assert hints == {}
97
+ assert priority is None
97
98
 
98
99
 
99
100
  def test_parse_sql_with_hints_merges_user_set():
100
- actual_sql, hints = _parse_sql_with_hints(
101
+ actual_sql, hints, priority = _parse_sql_with_hints(
101
102
  "SET odps.sql.type.system.odps2=true; SELECT 1"
102
103
  )
103
104
  assert actual_sql == "SELECT 1"
104
105
  assert hints == {
105
106
  "odps.sql.type.system.odps2": "true",
106
107
  }
108
+ assert priority is None
107
109
 
108
110
 
109
111
  def test_parse_sql_with_hints_blocks_write_without_force():
@@ -117,13 +119,14 @@ def test_parse_sql_with_hints_blocks_write_without_force():
117
119
 
118
120
 
119
121
  def test_parse_sql_with_hints_force_allows_write():
120
- actual_sql, hints = _parse_sql_with_hints("CREATE TABLE t (id BIGINT)", force=True)
122
+ actual_sql, hints, priority = _parse_sql_with_hints("CREATE TABLE t (id BIGINT)", force=True)
121
123
  assert actual_sql == "CREATE TABLE t (id BIGINT)"
122
124
  assert hints == {}
125
+ assert priority is None
123
126
 
124
127
 
125
128
  def test_parse_sql_with_hints_force_preserves_user_sets():
126
- actual_sql, hints = _parse_sql_with_hints(
129
+ actual_sql, hints, _priority = _parse_sql_with_hints(
127
130
  "SET odps.sql.type.system.odps2=true; CREATE TABLE t (id BIGINT)",
128
131
  force=True,
129
132
  )
@@ -136,6 +139,39 @@ def test_parse_sql_with_hints_invalid_set_raises():
136
139
  _parse_sql_with_hints("SET no_semicolon SELECT 1")
137
140
 
138
141
 
142
+ def test_parse_sql_with_hints_extracts_priority():
143
+ actual_sql, hints, priority = _parse_sql_with_hints(
144
+ "SET odps.instance.priority=3; SELECT 1"
145
+ )
146
+ assert actual_sql == "SELECT 1"
147
+ assert priority == 3
148
+ # priority must be stripped from the hints dict — it's a run_sql kwarg, not a SQL hint.
149
+ assert "odps.instance.priority" not in hints
150
+
151
+
152
+ def test_parse_sql_with_hints_priority_case_insensitive_key():
153
+ _, hints, priority = _parse_sql_with_hints(
154
+ "SET ODPS.Instance.Priority=5; SELECT 1"
155
+ )
156
+ assert priority == 5
157
+ assert hints == {}
158
+
159
+
160
+ def test_parse_sql_with_hints_priority_invalid_raises():
161
+ with pytest.raises(ValidationError, match="odps.instance.priority"):
162
+ _parse_sql_with_hints("SET odps.instance.priority=high; SELECT 1")
163
+
164
+
165
+ def test_parse_sql_with_hints_priority_coexists_with_other_hints():
166
+ _, hints, priority = _parse_sql_with_hints(
167
+ "SET odps.instance.priority=1; "
168
+ "SET odps.sql.type.system.odps2=true; "
169
+ "SELECT 1"
170
+ )
171
+ assert priority == 1
172
+ assert hints == {"odps.sql.type.system.odps2": "true"}
173
+
174
+
139
175
  # --- translate_odps_error readonly detection tests ---
140
176
 
141
177
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes