maxc-cli 0.2.1__tar.gz → 0.2.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 (64) hide show
  1. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/PKG-INFO +2 -3
  2. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/setup.py +2 -3
  3. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/__init__.py +1 -1
  4. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/auth_providers.py +16 -2
  5. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/cli.py +2 -0
  6. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/helpers.py +2 -0
  7. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/masking.py +8 -7
  8. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/setting_parser.py +2 -0
  9. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli.egg-info/PKG-INFO +2 -3
  10. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/tests/test_catalog.py +2 -1
  11. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/tests/test_external_auth.py +64 -8
  12. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/MANIFEST.in +0 -0
  13. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/README.md +0 -0
  14. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/pyproject.toml +0 -0
  15. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/scripts/regression_test.py +0 -0
  16. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/setup.cfg +0 -0
  17. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/__main__.py +0 -0
  18. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/app.py +0 -0
  19. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/audit.py +0 -0
  20. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/backend/__init__.py +0 -0
  21. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/backend/auth.py +0 -0
  22. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/backend/catalog.py +0 -0
  23. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/backend/data.py +0 -0
  24. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/backend/job.py +0 -0
  25. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/backend/meta.py +0 -0
  26. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/backend/odps.py +0 -0
  27. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/backend/query.py +0 -0
  28. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/cache.py +0 -0
  29. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/config.py +0 -0
  30. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/exceptions.py +0 -0
  31. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/models.py +0 -0
  32. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/output.py +0 -0
  33. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/skills/SKILL.md +0 -0
  34. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/skills/agents/openai.yaml +0 -0
  35. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/skills/references/bootstrap-auth.md +0 -0
  36. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/skills/references/bootstrap-flow.md +0 -0
  37. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/skills/references/command-patterns.md +0 -0
  38. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/skills/references/json-output-format.md +0 -0
  39. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/skills/references/maxcompute-sql-notes.md +0 -0
  40. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/skills/references/migrate-from-odpscmd.md +0 -0
  41. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/skills/references/partition-guide.md +0 -0
  42. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/skills/references/red-lines.md +0 -0
  43. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/skills/references/setup-install.md +0 -0
  44. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/store.py +0 -0
  45. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/utils.py +0 -0
  46. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli.egg-info/SOURCES.txt +0 -0
  47. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli.egg-info/dependency_links.txt +0 -0
  48. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli.egg-info/entry_points.txt +0 -0
  49. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli.egg-info/requires.txt +0 -0
  50. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli.egg-info/top_level.txt +0 -0
  51. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/tests/test_agent_hints_and_cli.py +0 -0
  52. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/tests/test_agent_skill_commands_context.py +0 -0
  53. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/tests/test_cache.py +0 -0
  54. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/tests/test_cli_mock.py +0 -0
  55. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/tests/test_compat.py +0 -0
  56. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/tests/test_e2e_smoke.py +0 -0
  57. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/tests/test_error_self_correction.py +0 -0
  58. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/tests/test_integration.py +0 -0
  59. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/tests/test_integration_real.py +0 -0
  60. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/tests/test_job_improvements.py +0 -0
  61. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/tests/test_masking.py +0 -0
  62. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/tests/test_phase1_improvements.py +0 -0
  63. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/tests/test_query_auto_promote.py +0 -0
  64. {maxc_cli-0.2.1 → maxc_cli-0.2.3}/tests/test_setting_parser.py +0 -0
@@ -1,16 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maxc-cli
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: Agent-native MaxCompute CLI for external coding agents
5
5
  Classifier: Programming Language :: Python :: 3
6
- Classifier: Programming Language :: Python :: 3.8
7
6
  Classifier: Programming Language :: Python :: 3.9
8
7
  Classifier: Programming Language :: Python :: 3.10
9
8
  Classifier: Programming Language :: Python :: 3.11
10
9
  Classifier: Programming Language :: Python :: 3.12
11
10
  Classifier: Programming Language :: Python :: 3.13
12
11
  Classifier: Programming Language :: Python :: 3.14
13
- Requires-Python: >=3.8
12
+ Requires-Python: >=3.9
14
13
  Description-Content-Type: text/markdown
15
14
  Requires-Dist: PyYAML>=5.4
16
15
  Requires-Dist: pyodps
@@ -9,11 +9,11 @@ README = ROOT / "README.md"
9
9
 
10
10
  setup(
11
11
  name="maxc-cli",
12
- version="0.2.1",
12
+ version="0.2.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",
16
- python_requires=">=3.8",
16
+ python_requires=">=3.9",
17
17
  package_dir={"": "src"},
18
18
  packages=find_packages(where="src"),
19
19
  include_package_data=True,
@@ -26,7 +26,6 @@ setup(
26
26
  },
27
27
  classifiers=[
28
28
  "Programming Language :: Python :: 3",
29
- "Programming Language :: Python :: 3.8",
30
29
  "Programming Language :: Python :: 3.9",
31
30
  "Programming Language :: Python :: 3.10",
32
31
  "Programming Language :: Python :: 3.11",
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.2.1"
5
+ __version__ = "0.2.3"
@@ -3,6 +3,7 @@ from dataclasses import dataclass, field
3
3
  from datetime import datetime, timedelta, timezone
4
4
  import hashlib
5
5
  import json
6
+ import logging
6
7
  from pathlib import Path
7
8
  import shlex
8
9
  import shutil
@@ -14,6 +15,8 @@ from .config import AuthConfig, ExternalAuthConfig, MaxCConfig, NcsAuthConfig
14
15
  from .exceptions import FeatureUnavailableError, ValidationError
15
16
  from .helpers import missing_odps_settings, odps_identity_source, resolve_odps_settings
16
17
 
18
+ logger = logging.getLogger(__name__)
19
+
17
20
 
18
21
  @dataclass
19
22
  class ResolvedAuthConnection:
@@ -486,7 +489,7 @@ class ExternalCredentialProvider:
486
489
  expires_at_str = payload.get("expires_at")
487
490
  if not expires_at_str:
488
491
  return None # no expiry → don't trust kv cache
489
- expires_at = datetime.fromisoformat(expires_at_str)
492
+ expires_at = datetime.fromisoformat(expires_at_str.replace("Z", "+00:00"))
490
493
  if expires_at.tzinfo is None:
491
494
  expires_at = expires_at.replace(tzinfo=timezone.utc)
492
495
  cutoff = expires_at - timedelta(seconds=self._EXPIRY_BUFFER_SECONDS)
@@ -517,6 +520,13 @@ class ExternalCredentialProvider:
517
520
  pass # kv_store write failure is non-fatal
518
521
 
519
522
  def get_credential(self) -> 'SimpleTempCredential':
523
+ try:
524
+ return self._get_credential_inner()
525
+ except Exception as exc:
526
+ logger.warning("ExternalCredentialProvider.get_credential() failed: %s", exc)
527
+ raise
528
+
529
+ def _get_credential_inner(self) -> 'SimpleTempCredential':
520
530
  # 1. In-process cache (fast path)
521
531
  with self._lock:
522
532
  if not self._is_expired():
@@ -537,7 +547,7 @@ class ExternalCredentialProvider:
537
547
  raw_expiry = payload.get("expires_at")
538
548
  if raw_expiry:
539
549
  try:
540
- expires_at = datetime.fromisoformat(raw_expiry)
550
+ expires_at = datetime.fromisoformat(raw_expiry.replace("Z", "+00:00"))
541
551
  if expires_at.tzinfo is None:
542
552
  expires_at = expires_at.replace(tzinfo=timezone.utc)
543
553
  except (ValueError, TypeError):
@@ -590,6 +600,10 @@ class ExternalCredentialProvider:
590
600
  def get_security_token(self) -> 'str | None':
591
601
  return self.get_credential().security_token
592
602
 
603
+ # Alias: pyodps CredentialProviderAccount falls back to get_credentials()
604
+ # when get_credential() raises, so both must exist.
605
+ get_credentials = get_credential
606
+
593
607
 
594
608
  def build_external_account(settings: 'dict[str, str | None]', *, cache: 'Any | None' = None):
595
609
  """Build a pyodps Account backed by an ExternalCredentialProvider."""
@@ -1,4 +1,6 @@
1
1
 
2
+ from __future__ import annotations
3
+
2
4
  import argparse
3
5
  import difflib
4
6
  import sys
@@ -1,5 +1,7 @@
1
1
  """Helper functions for MaxCompute backend operations."""
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  from collections import Counter
4
6
  from datetime import date, datetime, time, timezone
5
7
  from decimal import Decimal
@@ -1,9 +1,10 @@
1
+ from __future__ import annotations
1
2
 
2
3
  import re
3
- from typing import Any
4
+ from typing import Any, List, Optional
4
5
 
5
6
 
6
- SENSITIVE_PATTERNS: list[tuple[re.Pattern[str], str]] = [
7
+ SENSITIVE_PATTERNS: List[tuple[re.Pattern[str], str]] = [
7
8
  (re.compile(r"(?i)(password|passwd|secret|pwd|api_key)"), "password"),
8
9
  (re.compile(r"(?i)(phone|mobile|tel|cellphone)"), "phone"),
9
10
  (re.compile(r"(?i)(email|e_mail|mail_addr)"), "email"),
@@ -11,7 +12,7 @@ SENSITIVE_PATTERNS: list[tuple[re.Pattern[str], str]] = [
11
12
  ]
12
13
 
13
14
 
14
- def _classify_column(column_name: str, extra_patterns: list[str] | None = None) -> str | None:
15
+ def _classify_column(column_name: str, extra_patterns: Optional[List[str]] = None) -> Optional[str]:
15
16
  """Return the masking type for a column name, or None if not sensitive."""
16
17
  for pattern, mask_type in SENSITIVE_PATTERNS:
17
18
  if pattern.search(column_name):
@@ -63,10 +64,10 @@ _MASKERS: dict[str, Any] = {
63
64
 
64
65
 
65
66
  def mask_rows(
66
- rows: list[dict[str, Any]],
67
- schema: list[dict[str, Any]],
68
- extra_sensitive_columns: list[str] | None = None,
69
- ) -> tuple[list[dict[str, Any]], list[str]]:
67
+ rows: List[dict[str, Any]],
68
+ schema: List[dict[str, Any]],
69
+ extra_sensitive_columns: Optional[List[str]] = None,
70
+ ) -> tuple[List[dict[str, Any]], List[str]]:
70
71
  """Mask sensitive columns in query result rows.
71
72
 
72
73
  Returns (masked_rows, masked_column_names).
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from enum import Enum
2
4
  from typing import Dict, List, NamedTuple
3
5
 
@@ -1,16 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maxc-cli
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: Agent-native MaxCompute CLI for external coding agents
5
5
  Classifier: Programming Language :: Python :: 3
6
- Classifier: Programming Language :: Python :: 3.8
7
6
  Classifier: Programming Language :: Python :: 3.9
8
7
  Classifier: Programming Language :: Python :: 3.10
9
8
  Classifier: Programming Language :: Python :: 3.11
10
9
  Classifier: Programming Language :: Python :: 3.12
11
10
  Classifier: Programming Language :: Python :: 3.13
12
11
  Classifier: Programming Language :: Python :: 3.14
13
- Requires-Python: >=3.8
12
+ Requires-Python: >=3.9
14
13
  Description-Content-Type: text/markdown
15
14
  Requires-Dist: PyYAML>=5.4
16
15
  Requires-Dist: pyodps
@@ -4,6 +4,7 @@ catalog_search_tables — with kv_store caching behaviour."""
4
4
  import json
5
5
  from datetime import datetime, timedelta, timezone
6
6
  from pathlib import Path
7
+ from typing import Optional
7
8
  from unittest.mock import MagicMock, PropertyMock, patch
8
9
 
9
10
  import pytest
@@ -24,7 +25,7 @@ def _make_cache(tmp_path: Path) -> LocalCache:
24
25
  return LocalCache(cache_dir)
25
26
 
26
27
 
27
- def _make_backend(*, cache: LocalCache | None = None, project: str = "test_proj") -> CatalogMixin:
28
+ def _make_backend(*, cache: Optional[LocalCache] = None, project: str = "test_proj") -> CatalogMixin:
28
29
  """Create a minimal CatalogMixin instance with mocked ODPS client."""
29
30
  mixin = CatalogMixin()
30
31
 
@@ -2,6 +2,7 @@
2
2
  infer_auth_provider external branch."""
3
3
 
4
4
  import json
5
+ import logging
5
6
  import os
6
7
  import stat
7
8
  import subprocess
@@ -9,6 +10,7 @@ import tempfile
9
10
  from datetime import datetime, timedelta, timezone
10
11
  from io import StringIO
11
12
  from pathlib import Path
13
+ from typing import Optional
12
14
  from unittest.mock import MagicMock, patch
13
15
 
14
16
  import pytest
@@ -40,10 +42,10 @@ def _make_cache(tmp_path: Path) -> LocalCache:
40
42
 
41
43
  def _minimal_config(
42
44
  *,
43
- provider: str | None = None,
44
- ext_command: str | None = None,
45
- project: str | None = None,
46
- endpoint: str | None = None,
45
+ provider: Optional[str] = None,
46
+ ext_command: Optional[str] = None,
47
+ project: Optional[str] = None,
48
+ endpoint: Optional[str] = None,
47
49
  ) -> MaxCConfig:
48
50
  """Create a MaxCConfig with minimal required fields for auth testing."""
49
51
  from maxc_cli.config import AgentConfig
@@ -77,8 +79,8 @@ def _cred_json(
77
79
  *,
78
80
  access_key_id: str = "LTAI_TEST",
79
81
  access_key_secret: str = "secret_test",
80
- security_token: str | None = None,
81
- expiration: str | None = None,
82
+ security_token: Optional[str] = None,
83
+ expiration: Optional[str] = None,
82
84
  ) -> str:
83
85
  """Build credential JSON string for echo commands."""
84
86
  payload: dict = {
@@ -437,8 +439,8 @@ class TestBuildExternalAccount:
437
439
 
438
440
  class TestInferAuthProviderExternal:
439
441
 
440
- def _make_config(self, *, provider: str | None = None,
441
- ext_command: str | None = None) -> MaxCConfig:
442
+ def _make_config(self, *, provider: Optional[str] = None,
443
+ ext_command: Optional[str] = None) -> MaxCConfig:
442
444
  return _minimal_config(provider=provider, ext_command=ext_command)
443
445
 
444
446
  def test_explicit_external_provider(self):
@@ -662,3 +664,57 @@ class TestNcsToExternalMigration:
662
664
  conn = resolve_auth_connection(config)
663
665
  assert conn.auth_type == "external"
664
666
  assert conn.provider == "external"
667
+
668
+
669
+ # ============================================================
670
+ # get_credentials alias + exception logging
671
+ # ============================================================
672
+
673
+ class TestGetCredentialsAliasAndExceptionLogging:
674
+ """Verify that get_credentials() works (pyodps fallback) and that
675
+ original exceptions are logged instead of being silently swallowed."""
676
+
677
+ def test_get_credentials_is_alias_of_get_credential(self):
678
+ """get_credentials() returns the same result as get_credential()."""
679
+ cmd = _echo_cmd(_cred_json())
680
+ p = ExternalCredentialProvider(command=cmd, timeout=10)
681
+ c1 = p.get_credential()
682
+ c2 = p.get_credentials()
683
+ assert c1.access_key_id == c2.access_key_id
684
+ assert c1.access_key_secret == c2.access_key_secret
685
+
686
+ def test_get_credentials_propagates_original_error(self):
687
+ """When the command fails, get_credentials() raises the original
688
+ ValidationError — not AttributeError."""
689
+ cmd = "exit 42"
690
+ p = ExternalCredentialProvider(command=cmd, timeout=10)
691
+ with pytest.raises(ValidationError, match="exited with code 42"):
692
+ p.get_credentials()
693
+
694
+ def test_get_credential_logs_original_error(self, caplog):
695
+ """When get_credential() fails, the original error is logged
696
+ at WARNING level so it is not silently swallowed by pyodps'
697
+ bare ``except`` in CredentialProviderAccount._refresh_credential."""
698
+ cmd = "exit 42"
699
+ p = ExternalCredentialProvider(command=cmd, timeout=10)
700
+ with caplog.at_level(logging.WARNING, logger="maxc_cli.auth_providers"):
701
+ with pytest.raises(ValidationError, match="exited with code 42"):
702
+ p.get_credential()
703
+ assert any("get_credential() failed" in rec.message for rec in caplog.records)
704
+
705
+ def test_credential_provider_account_fallback_uses_get_credentials(self):
706
+ """Integration test: CredentialProviderAccount._refresh_credential
707
+ falls back from get_credential → get_credentials when the first
708
+ call raises. With our alias, both calls raise the same
709
+ ValidationError (not AttributeError)."""
710
+ try:
711
+ from odps.accounts import CredentialProviderAccount
712
+ except ImportError:
713
+ pytest.skip("pyodps not installed")
714
+
715
+ cmd = "exit 42"
716
+ provider = ExternalCredentialProvider(command=cmd, timeout=10)
717
+ account = CredentialProviderAccount(provider)
718
+
719
+ with pytest.raises(ValidationError, match="exited with code 42"):
720
+ account._refresh_credential()
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