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.
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/PKG-INFO +2 -3
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/setup.py +2 -3
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/__init__.py +1 -1
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/auth_providers.py +16 -2
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/cli.py +2 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/helpers.py +2 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/masking.py +8 -7
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/setting_parser.py +2 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli.egg-info/PKG-INFO +2 -3
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/tests/test_catalog.py +2 -1
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/tests/test_external_auth.py +64 -8
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/MANIFEST.in +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/README.md +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/pyproject.toml +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/scripts/regression_test.py +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/setup.cfg +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/__main__.py +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/app.py +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/audit.py +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/backend/__init__.py +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/backend/auth.py +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/backend/catalog.py +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/backend/data.py +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/backend/job.py +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/backend/meta.py +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/backend/odps.py +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/backend/query.py +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/cache.py +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/config.py +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/exceptions.py +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/models.py +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/output.py +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/skills/SKILL.md +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/skills/agents/openai.yaml +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/skills/references/bootstrap-auth.md +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/skills/references/bootstrap-flow.md +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/skills/references/command-patterns.md +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/skills/references/json-output-format.md +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/skills/references/maxcompute-sql-notes.md +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/skills/references/migrate-from-odpscmd.md +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/skills/references/partition-guide.md +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/skills/references/red-lines.md +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/skills/references/setup-install.md +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/store.py +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli/utils.py +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli.egg-info/SOURCES.txt +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli.egg-info/dependency_links.txt +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli.egg-info/entry_points.txt +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli.egg-info/requires.txt +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/src/maxc_cli.egg-info/top_level.txt +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/tests/test_agent_hints_and_cli.py +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/tests/test_agent_skill_commands_context.py +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/tests/test_cache.py +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/tests/test_cli_mock.py +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/tests/test_compat.py +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/tests/test_e2e_smoke.py +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/tests/test_error_self_correction.py +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/tests/test_integration.py +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/tests/test_integration_real.py +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/tests/test_job_improvements.py +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/tests/test_masking.py +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/tests/test_phase1_improvements.py +0 -0
- {maxc_cli-0.2.1 → maxc_cli-0.2.3}/tests/test_query_auto_promote.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|
|
@@ -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,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:
|
|
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:
|
|
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:
|
|
67
|
-
schema:
|
|
68
|
-
extra_sensitive_columns:
|
|
69
|
-
) -> tuple[
|
|
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,16 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: maxc-cli
|
|
3
|
-
Version: 0.2.
|
|
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.
|
|
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
|
|
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
|
|
44
|
-
ext_command: str
|
|
45
|
-
project: str
|
|
46
|
-
endpoint: str
|
|
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
|
|
81
|
-
expiration: str
|
|
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
|
|
441
|
-
ext_command: str
|
|
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
|
|
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
|
|
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
|
|
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
|