adaptorch-cli 0.1.0__tar.gz → 0.1.1__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 (23) hide show
  1. {adaptorch_cli-0.1.0/src/adaptorch_cli.egg-info → adaptorch_cli-0.1.1}/PKG-INFO +2 -2
  2. {adaptorch_cli-0.1.0 → adaptorch_cli-0.1.1}/pyproject.toml +2 -2
  3. {adaptorch_cli-0.1.0 → adaptorch_cli-0.1.1}/src/adaptorch_cli/cli.py +32 -3
  4. {adaptorch_cli-0.1.0 → adaptorch_cli-0.1.1}/src/adaptorch_cli/config_store.py +38 -2
  5. {adaptorch_cli-0.1.0 → adaptorch_cli-0.1.1}/src/adaptorch_cli/parser.py +23 -1
  6. {adaptorch_cli-0.1.0 → adaptorch_cli-0.1.1/src/adaptorch_cli.egg-info}/PKG-INFO +2 -2
  7. {adaptorch_cli-0.1.0 → adaptorch_cli-0.1.1}/src/adaptorch_cli.egg-info/SOURCES.txt +2 -1
  8. adaptorch_cli-0.1.1/src/adaptorch_cli.egg-info/requires.txt +1 -0
  9. {adaptorch_cli-0.1.0 → adaptorch_cli-0.1.1}/tests/test_cli_commands.py +37 -0
  10. adaptorch_cli-0.1.1/tests/test_cli_oauth.py +76 -0
  11. adaptorch_cli-0.1.0/src/adaptorch_cli.egg-info/requires.txt +0 -1
  12. {adaptorch_cli-0.1.0 → adaptorch_cli-0.1.1}/LICENSE +0 -0
  13. {adaptorch_cli-0.1.0 → adaptorch_cli-0.1.1}/README.md +0 -0
  14. {adaptorch_cli-0.1.0 → adaptorch_cli-0.1.1}/setup.cfg +0 -0
  15. {adaptorch_cli-0.1.0 → adaptorch_cli-0.1.1}/src/adaptorch_cli/__init__.py +0 -0
  16. {adaptorch_cli-0.1.0 → adaptorch_cli-0.1.1}/src/adaptorch_cli/__main__.py +0 -0
  17. {adaptorch_cli-0.1.0 → adaptorch_cli-0.1.1}/src/adaptorch_cli/py.typed +0 -0
  18. {adaptorch_cli-0.1.0 → adaptorch_cli-0.1.1}/src/adaptorch_cli.egg-info/dependency_links.txt +0 -0
  19. {adaptorch_cli-0.1.0 → adaptorch_cli-0.1.1}/src/adaptorch_cli.egg-info/entry_points.txt +0 -0
  20. {adaptorch_cli-0.1.0 → adaptorch_cli-0.1.1}/src/adaptorch_cli.egg-info/top_level.txt +0 -0
  21. {adaptorch_cli-0.1.0 → adaptorch_cli-0.1.1}/tests/test_cli_boundary.py +0 -0
  22. {adaptorch_cli-0.1.0 → adaptorch_cli-0.1.1}/tests/test_cli_integration.py +0 -0
  23. {adaptorch_cli-0.1.0 → adaptorch_cli-0.1.1}/tests/test_cli_login_config.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: adaptorch-cli
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: Command-line client for AdaptOrch
5
5
  Author: ClassicMate
6
6
  License-Expression: LicenseRef-Proprietary
@@ -11,7 +11,7 @@ Project-URL: Documentation, https://adaptorch.com/mcp-docs
11
11
  Requires-Python: >=3.11
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
- Requires-Dist: adaptorch-client<0.2,>=0.1
14
+ Requires-Dist: adaptorch-client<0.2,>=0.1.2
15
15
  Dynamic: license-file
16
16
 
17
17
  # adaptorch-cli
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "adaptorch-cli"
7
- version = "0.1.0"
7
+ version = "0.1.1"
8
8
  description = "Command-line client for AdaptOrch"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
11
11
  license = "LicenseRef-Proprietary"
12
12
  license-files = ["LICENSE"]
13
13
  authors = [{ name = "ClassicMate" }]
14
- dependencies = ["adaptorch-client>=0.1,<0.2"]
14
+ dependencies = ["adaptorch-client>=0.1.2,<0.2"]
15
15
 
16
16
  [project.urls]
17
17
  Homepage = "https://adaptorch.com"
@@ -13,6 +13,7 @@ from adaptorch_client import (
13
13
  AdaptOrchAPIError,
14
14
  AdaptOrchClient,
15
15
  ClientConfig,
16
+ PollPolicy,
16
17
  ProviderCredential,
17
18
  validate_api_url,
18
19
  )
@@ -39,7 +40,10 @@ def _reject_json_constant(_value: str) -> Never:
39
40
 
40
41
 
41
42
  def _finite_float(text: str) -> float:
42
- value = float(text)
43
+ try:
44
+ value = float(text)
45
+ except ValueError:
46
+ _reject_json_constant(text)
43
47
  if not math.isfinite(value):
44
48
  raise ValueError("non-finite JSON number")
45
49
  return value
@@ -97,11 +101,14 @@ def _require_client(api_url: str) -> AdaptOrchClient:
97
101
 
98
102
 
99
103
  def _submission_credential() -> ProviderCredential | None:
100
- resolved, _source = config_store.resolve_provider_credential()
104
+ resolved, source = config_store.resolve_provider_credential()
101
105
  if resolved is None:
102
106
  return None
103
107
  provider, model, key = resolved
104
- return ProviderCredential(provider, model, key)
108
+ auth_type, account_id = config_store.resolve_provider_auth(source)
109
+ if auth_type is None and account_id is None:
110
+ return ProviderCredential(provider, model, key)
111
+ return ProviderCredential(provider, model, key, auth_type=auth_type, account_id=account_id)
105
112
 
106
113
 
107
114
  def _read_secret(prompt: str, *, stdin_flag: bool) -> str:
@@ -241,6 +248,28 @@ def _run_command(
241
248
  case "get":
242
249
  run_id: str = args.run_id
243
250
  return _result_payload(_require_client(api_url).get_run(run_id)), True
251
+ case "wait":
252
+ wait_run_id: str = args.run_id
253
+ timeout_seconds: float = args.timeout
254
+ interval_seconds: float = args.interval
255
+ if not math.isfinite(timeout_seconds) or timeout_seconds <= 0:
256
+ parser.error("--timeout must be finite positive seconds")
257
+ if not math.isfinite(interval_seconds) or interval_seconds <= 0:
258
+ parser.error("--interval must be finite positive seconds")
259
+ wait_result = _require_client(api_url).wait_for_run(
260
+ wait_run_id,
261
+ policy=PollPolicy(
262
+ timeout_seconds=timeout_seconds, interval_seconds=interval_seconds
263
+ ),
264
+ )
265
+ wait_payload: dict[str, JSONValue] = {
266
+ "reason": wait_result.reason.value,
267
+ "polls": wait_result.polls,
268
+ "elapsed_seconds": wait_result.elapsed_seconds,
269
+ }
270
+ if wait_result.run is not None:
271
+ wait_payload["run"] = wait_result.run.to_payload()
272
+ return wait_payload, True
244
273
  case "cancel":
245
274
  cancel_run_id: str = args.run_id
246
275
  reason: str | None = args.reason
@@ -27,6 +27,8 @@ _PROVIDER_KEY_MAP = {
27
27
  "provider.name": "provider",
28
28
  "provider.model": "model",
29
29
  "provider.api_key": "api_key",
30
+ "provider.auth_type": "auth_type",
31
+ "provider.account_id": "account_id",
30
32
  }
31
33
  _CONFIGURABLE_KEYS = frozenset({"api_url", *_PROVIDER_KEY_MAP})
32
34
 
@@ -175,18 +177,50 @@ def resolve_provider_credential() -> tuple[tuple[str, str, str] | None, str]:
175
177
  env_provider = os.environ.get("ADAPTORCH_PROVIDER", "").strip()
176
178
  env_model = os.environ.get("ADAPTORCH_PROVIDER_MODEL", "").strip()
177
179
  env_key = os.environ.get("ADAPTORCH_PROVIDER_API_KEY", "").strip()
178
- if env_provider or env_model or env_key:
180
+ if any(
181
+ (
182
+ env_provider,
183
+ env_model,
184
+ env_key,
185
+ os.environ.get("ADAPTORCH_PROVIDER_AUTH_TYPE"),
186
+ os.environ.get("ADAPTORCH_PROVIDER_ACCOUNT_ID"),
187
+ )
188
+ ):
179
189
  return (env_provider, env_model, env_key), "env"
180
190
  provider_cfg = load_config().get("provider")
181
191
  if isinstance(provider_cfg, dict):
182
192
  provider = str(provider_cfg.get("provider") or "").strip()
183
193
  model = str(provider_cfg.get("model") or "").strip()
184
194
  key = str(provider_cfg.get("api_key") or "").strip()
185
- if provider or model or key:
195
+ has_auth_metadata = "auth_type" in provider_cfg or "account_id" in provider_cfg
196
+ if provider or model or key or has_auth_metadata:
186
197
  return (provider, model, key), "config"
187
198
  return None, "none"
188
199
 
189
200
 
201
+ def resolve_provider_auth(source: str) -> tuple[str | None, str | None]:
202
+ """Read metadata from the same source as the token; never mix identities."""
203
+ if source == "env":
204
+ values = (
205
+ os.environ.get("ADAPTORCH_PROVIDER_AUTH_TYPE"),
206
+ os.environ.get("ADAPTORCH_PROVIDER_ACCOUNT_ID"),
207
+ )
208
+ else:
209
+ provider = load_config().get("provider")
210
+ if not isinstance(provider, dict):
211
+ return None, None
212
+ values = (provider.get("auth_type"), provider.get("account_id"))
213
+
214
+ def optional_text(value: object) -> str | None:
215
+ if value is None:
216
+ return None
217
+ if not isinstance(value, str):
218
+ raise ValueError("provider authentication metadata must be strings")
219
+ return value.strip() or None
220
+
221
+ return optional_text(values[0]), optional_text(values[1])
222
+
223
+
190
224
  def mask_secret(value: str | None) -> str | None:
191
225
  if not value:
192
226
  return None
@@ -209,6 +243,8 @@ def config_view() -> dict[str, Any]:
209
243
  "name": provider.get("provider"),
210
244
  "model": provider.get("model"),
211
245
  "api_key": mask_secret(provider_key if isinstance(provider_key, str) else None),
246
+ "auth_type": provider.get("auth_type"),
247
+ "account_id_configured": bool(provider.get("account_id")),
212
248
  "source": resolve_provider_credential()[1],
213
249
  },
214
250
  "config_path": str(config_path()),
@@ -33,6 +33,28 @@ def _add_run_commands(parent: argparse._SubParsersAction[argparse.ArgumentParser
33
33
  get = commands.add_parser("get", help="Get a run")
34
34
  get.add_argument("run_id")
35
35
 
36
+ wait = commands.add_parser(
37
+ "wait",
38
+ help=(
39
+ "Read an existing run until it reaches a terminal status — the "
40
+ "CLI's form of the MCP resume_run_id contract. It never submits "
41
+ "another run; use it after an uncertain submit response."
42
+ ),
43
+ )
44
+ wait.add_argument("run_id")
45
+ wait.add_argument(
46
+ "--timeout",
47
+ type=float,
48
+ default=120.0,
49
+ help="Seconds to keep polling before stopping (default: 120)",
50
+ )
51
+ wait.add_argument(
52
+ "--interval",
53
+ type=float,
54
+ default=1.0,
55
+ help="Seconds between status reads (default: 1)",
56
+ )
57
+
36
58
  cancel = commands.add_parser("cancel", help="Cancel a run")
37
59
  cancel.add_argument("run_id")
38
60
  cancel.add_argument("--reason")
@@ -76,7 +98,7 @@ def build_parser() -> argparse.ArgumentParser:
76
98
  config_commands.add_parser("get", help="Show the merged configuration (secrets masked)")
77
99
  config_set = config_commands.add_parser(
78
100
  "set",
79
- help="Set api_url, provider.name, provider.model, or provider.api_key",
101
+ help="Set api_url or provider.name/model/api_key/auth_type/account_id",
80
102
  )
81
103
  config_set.add_argument("key")
82
104
  config_set.add_argument("value", nargs="?")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: adaptorch-cli
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: Command-line client for AdaptOrch
5
5
  Author: ClassicMate
6
6
  License-Expression: LicenseRef-Proprietary
@@ -11,7 +11,7 @@ Project-URL: Documentation, https://adaptorch.com/mcp-docs
11
11
  Requires-Python: >=3.11
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
- Requires-Dist: adaptorch-client<0.2,>=0.1
14
+ Requires-Dist: adaptorch-client<0.2,>=0.1.2
15
15
  Dynamic: license-file
16
16
 
17
17
  # adaptorch-cli
@@ -16,4 +16,5 @@ src/adaptorch_cli.egg-info/top_level.txt
16
16
  tests/test_cli_boundary.py
17
17
  tests/test_cli_commands.py
18
18
  tests/test_cli_integration.py
19
- tests/test_cli_login_config.py
19
+ tests/test_cli_login_config.py
20
+ tests/test_cli_oauth.py
@@ -0,0 +1 @@
1
+ adaptorch-client<0.2,>=0.1.2
@@ -118,6 +118,43 @@ def test_run_get_prints_client_json(run_cli: CliRunner) -> None:
118
118
  assert _json_stdout(result.stdout) == {"id": "run-1", "status": "RUNNING"}
119
119
 
120
120
 
121
+ def test_run_wait_reads_until_terminal_without_resubmitting(run_cli: CliRunner) -> None:
122
+ result = run_cli(
123
+ [
124
+ "--output",
125
+ "json",
126
+ "run",
127
+ "wait",
128
+ "run-1",
129
+ "--timeout",
130
+ "5",
131
+ "--interval",
132
+ "0.5",
133
+ ]
134
+ )
135
+
136
+ assert result.returncode == 0
137
+ assert _json_stdout(result.stdout) == {
138
+ "reason": "terminal",
139
+ "polls": 1,
140
+ "elapsed_seconds": 0.01,
141
+ "run": {"id": "run-1", "status": "SUCCEEDED"},
142
+ }
143
+ calls = [json.loads(line) for line in result.client_log.splitlines()]
144
+ assert calls[-1] == {
145
+ "args": ["run-1"],
146
+ "kwargs": {"interval_seconds": 0.5, "timeout_seconds": 5.0},
147
+ "method": "wait_for_run",
148
+ }
149
+
150
+
151
+ def test_run_wait_rejects_non_positive_bounds(run_cli: CliRunner) -> None:
152
+ result = run_cli(["--output", "json", "run", "wait", "run-1", "--timeout", "0"])
153
+
154
+ assert result.returncode != 0
155
+ assert result.client_log.count("wait_for_run") == 0
156
+
157
+
121
158
  def test_run_cancel_prints_client_json(run_cli: CliRunner) -> None:
122
159
  result = run_cli(["--output", "json", "run", "cancel", "run-1"])
123
160
 
@@ -0,0 +1,76 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import pytest
7
+
8
+ from adaptorch_cli import cli, config_store
9
+
10
+
11
+ @pytest.fixture(autouse=True)
12
+ def isolated_config(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
13
+ monkeypatch.setenv("ADAPTORCH_CONFIG_DIR", str(tmp_path))
14
+ for name in (
15
+ "ADAPTORCH_PROVIDER",
16
+ "ADAPTORCH_PROVIDER_MODEL",
17
+ "ADAPTORCH_PROVIDER_API_KEY",
18
+ "ADAPTORCH_PROVIDER_AUTH_TYPE",
19
+ "ADAPTORCH_PROVIDER_ACCOUNT_ID",
20
+ ):
21
+ monkeypatch.delenv(name, raising=False)
22
+
23
+
24
+ def test_cli_resolves_oauth_from_environment_without_persisting_token(
25
+ monkeypatch: pytest.MonkeyPatch,
26
+ ) -> None:
27
+ for name, value in {
28
+ "ADAPTORCH_PROVIDER": "openai_codex",
29
+ "ADAPTORCH_PROVIDER_MODEL": "gpt-5-codex",
30
+ "ADAPTORCH_PROVIDER_API_KEY": "synthetic-oauth-token",
31
+ "ADAPTORCH_PROVIDER_AUTH_TYPE": "oauth",
32
+ "ADAPTORCH_PROVIDER_ACCOUNT_ID": "caller-account",
33
+ }.items():
34
+ monkeypatch.setenv(name, value)
35
+ credential = cli._submission_credential()
36
+ assert credential is not None
37
+ assert credential.headers["X-Provider-Auth-Type"] == "oauth"
38
+ assert credential.headers["X-Provider-Account-Id"] == "caller-account"
39
+ assert not config_store.config_path().exists()
40
+
41
+
42
+ def test_cli_environment_credential_never_borrows_account_from_config(
43
+ monkeypatch: pytest.MonkeyPatch,
44
+ ) -> None:
45
+ config_store.save_config(
46
+ {
47
+ "provider": {
48
+ "provider": "openai_codex",
49
+ "model": "gpt-5-codex",
50
+ "api_key": "old-token",
51
+ "auth_type": "oauth",
52
+ "account_id": "old-account",
53
+ }
54
+ }
55
+ )
56
+ monkeypatch.setenv("ADAPTORCH_PROVIDER", "openai_codex")
57
+ monkeypatch.setenv("ADAPTORCH_PROVIDER_MODEL", "gpt-5-codex")
58
+ monkeypatch.setenv("ADAPTORCH_PROVIDER_API_KEY", "new-token")
59
+ credential = cli._submission_credential()
60
+ assert credential is not None
61
+ assert credential.account_id is None
62
+ assert credential.api_key == "new-token"
63
+
64
+
65
+ def test_cli_oauth_config_keys_and_view_are_safe() -> None:
66
+ config_store.set_config_value("provider.name", "openai_codex")
67
+ config_store.set_config_value("provider.model", "gpt-5-codex")
68
+ config_store.set_config_value("provider.api_key", "synthetic-oauth-token")
69
+ config_store.set_config_value("provider.auth_type", "oauth")
70
+ config_store.set_config_value("provider.account_id", "caller-account")
71
+ credential = cli._submission_credential()
72
+ assert credential is not None and credential.account_id == "caller-account"
73
+ view = json.dumps(config_store.config_view())
74
+ assert "synthetic-oauth-token" not in view
75
+ assert "caller-account" not in view
76
+ assert config_store.config_view()["provider"]["account_id_configured"] is True
@@ -1 +0,0 @@
1
- adaptorch-client<0.2,>=0.1
File without changes
File without changes
File without changes