adaptorch-cli 0.1.0__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.
- adaptorch_cli-0.1.0/LICENSE +14 -0
- adaptorch_cli-0.1.0/PKG-INFO +46 -0
- adaptorch_cli-0.1.0/README.md +30 -0
- adaptorch_cli-0.1.0/pyproject.toml +40 -0
- adaptorch_cli-0.1.0/setup.cfg +4 -0
- adaptorch_cli-0.1.0/src/adaptorch_cli/__init__.py +1 -0
- adaptorch_cli-0.1.0/src/adaptorch_cli/__main__.py +3 -0
- adaptorch_cli-0.1.0/src/adaptorch_cli/cli.py +351 -0
- adaptorch_cli-0.1.0/src/adaptorch_cli/config_store.py +215 -0
- adaptorch_cli-0.1.0/src/adaptorch_cli/parser.py +115 -0
- adaptorch_cli-0.1.0/src/adaptorch_cli/py.typed +0 -0
- adaptorch_cli-0.1.0/src/adaptorch_cli.egg-info/PKG-INFO +46 -0
- adaptorch_cli-0.1.0/src/adaptorch_cli.egg-info/SOURCES.txt +19 -0
- adaptorch_cli-0.1.0/src/adaptorch_cli.egg-info/dependency_links.txt +1 -0
- adaptorch_cli-0.1.0/src/adaptorch_cli.egg-info/entry_points.txt +2 -0
- adaptorch_cli-0.1.0/src/adaptorch_cli.egg-info/requires.txt +1 -0
- adaptorch_cli-0.1.0/src/adaptorch_cli.egg-info/top_level.txt +1 -0
- adaptorch_cli-0.1.0/tests/test_cli_boundary.py +279 -0
- adaptorch_cli-0.1.0/tests/test_cli_commands.py +141 -0
- adaptorch_cli-0.1.0/tests/test_cli_integration.py +133 -0
- adaptorch_cli-0.1.0/tests/test_cli_login_config.py +301 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Copyright (c) 2026 ClassicMate. All rights reserved.
|
|
2
|
+
|
|
3
|
+
This software and associated documentation files (the "Software") are proprietary
|
|
4
|
+
and confidential. Unauthorized copying, distribution, modification, public
|
|
5
|
+
display, or any use of the Software other than as expressly authorized by ClassicMate
|
|
6
|
+
is strictly prohibited.
|
|
7
|
+
|
|
8
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
9
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
10
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
11
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
12
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
13
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
14
|
+
SOFTWARE.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: adaptorch-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Command-line client for AdaptOrch
|
|
5
|
+
Author: ClassicMate
|
|
6
|
+
License-Expression: LicenseRef-Proprietary
|
|
7
|
+
Project-URL: Homepage, https://adaptorch.com
|
|
8
|
+
Project-URL: Repository, https://github.com/dmae97/Adaptorch-MCP
|
|
9
|
+
Project-URL: Issues, https://github.com/dmae97/Adaptorch-MCP/issues
|
|
10
|
+
Project-URL: Documentation, https://adaptorch.com/mcp-docs
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: adaptorch-client<0.2,>=0.1
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# adaptorch-cli
|
|
18
|
+
|
|
19
|
+
Independent AdaptOrch SaaS CLI. Installs the `adaptorchctl` command and uses `adaptorch-client`; it does not invoke `adaptorch` or `adaptorch-mcp`.
|
|
20
|
+
|
|
21
|
+
Log in once and every command picks the credential up:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
adaptorchctl auth login # prompts for the key, verifies it, stores it at
|
|
25
|
+
# ~/.config/adaptorch/config.json (mode 0600)
|
|
26
|
+
adaptorchctl auth status # shows credential_source: env | config | none
|
|
27
|
+
adaptorchctl auth logout # removes the stored key
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`ADAPTORCH_API_KEY` still works and always wins over the stored key, so CI
|
|
31
|
+
keeps injecting secrets via the environment. `config set provider.{name,model,
|
|
32
|
+
api_key}` persists BYOK wiring for `run submit`; the env vars
|
|
33
|
+
`ADAPTORCH_PROVIDER`, `ADAPTORCH_PROVIDER_MODEL`, and
|
|
34
|
+
`ADAPTORCH_PROVIDER_API_KEY` override the stored values. Provider credentials
|
|
35
|
+
become `X-Provider`, `X-Provider-Model`, and `X-Provider-Key` only on
|
|
36
|
+
`run submit`; reads and cancellation never receive the provider key.
|
|
37
|
+
`ADAPTORCH_API_KEY` remains the separate tenant key.
|
|
38
|
+
|
|
39
|
+
Install the current `adaptorch-client` and `adaptorch-cli` source packages together.
|
|
40
|
+
A git push does not replace a previously published PyPI version.
|
|
41
|
+
|
|
42
|
+
See [`../../docs/adaptorchctl-usage.ko.md`](../../docs/adaptorchctl-usage.ko.md) for usage.
|
|
43
|
+
|
|
44
|
+
## License
|
|
45
|
+
|
|
46
|
+
Proprietary — Copyright ClassicMate. All rights reserved. See [LICENSE](https://github.com/dmae97/Adaptorch-MCP/blob/main/LICENSE).
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# adaptorch-cli
|
|
2
|
+
|
|
3
|
+
Independent AdaptOrch SaaS CLI. Installs the `adaptorchctl` command and uses `adaptorch-client`; it does not invoke `adaptorch` or `adaptorch-mcp`.
|
|
4
|
+
|
|
5
|
+
Log in once and every command picks the credential up:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
adaptorchctl auth login # prompts for the key, verifies it, stores it at
|
|
9
|
+
# ~/.config/adaptorch/config.json (mode 0600)
|
|
10
|
+
adaptorchctl auth status # shows credential_source: env | config | none
|
|
11
|
+
adaptorchctl auth logout # removes the stored key
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`ADAPTORCH_API_KEY` still works and always wins over the stored key, so CI
|
|
15
|
+
keeps injecting secrets via the environment. `config set provider.{name,model,
|
|
16
|
+
api_key}` persists BYOK wiring for `run submit`; the env vars
|
|
17
|
+
`ADAPTORCH_PROVIDER`, `ADAPTORCH_PROVIDER_MODEL`, and
|
|
18
|
+
`ADAPTORCH_PROVIDER_API_KEY` override the stored values. Provider credentials
|
|
19
|
+
become `X-Provider`, `X-Provider-Model`, and `X-Provider-Key` only on
|
|
20
|
+
`run submit`; reads and cancellation never receive the provider key.
|
|
21
|
+
`ADAPTORCH_API_KEY` remains the separate tenant key.
|
|
22
|
+
|
|
23
|
+
Install the current `adaptorch-client` and `adaptorch-cli` source packages together.
|
|
24
|
+
A git push does not replace a previously published PyPI version.
|
|
25
|
+
|
|
26
|
+
See [`../../docs/adaptorchctl-usage.ko.md`](../../docs/adaptorchctl-usage.ko.md) for usage.
|
|
27
|
+
|
|
28
|
+
## License
|
|
29
|
+
|
|
30
|
+
Proprietary — Copyright ClassicMate. All rights reserved. See [LICENSE](https://github.com/dmae97/Adaptorch-MCP/blob/main/LICENSE).
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "adaptorch-cli"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Command-line client for AdaptOrch"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "LicenseRef-Proprietary"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "ClassicMate" }]
|
|
14
|
+
dependencies = ["adaptorch-client>=0.1,<0.2"]
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://adaptorch.com"
|
|
18
|
+
Repository = "https://github.com/dmae97/Adaptorch-MCP"
|
|
19
|
+
Issues = "https://github.com/dmae97/Adaptorch-MCP/issues"
|
|
20
|
+
Documentation = "https://adaptorch.com/mcp-docs"
|
|
21
|
+
|
|
22
|
+
[project.scripts]
|
|
23
|
+
adaptorchctl = "adaptorch_cli.cli:entrypoint"
|
|
24
|
+
|
|
25
|
+
[tool.setuptools.packages.find]
|
|
26
|
+
where = ["src"]
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.package-data]
|
|
29
|
+
adaptorch_cli = ["py.typed"]
|
|
30
|
+
|
|
31
|
+
[tool.ruff]
|
|
32
|
+
line-length = 100
|
|
33
|
+
target-version = "py311"
|
|
34
|
+
|
|
35
|
+
[tool.ruff.lint]
|
|
36
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
37
|
+
|
|
38
|
+
[tool.mypy]
|
|
39
|
+
python_version = "3.11"
|
|
40
|
+
strict = true
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""AdaptOrch command-line client."""
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import math
|
|
6
|
+
import sys
|
|
7
|
+
import uuid
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Never, Protocol, TypeAlias
|
|
11
|
+
|
|
12
|
+
from adaptorch_client import (
|
|
13
|
+
AdaptOrchAPIError,
|
|
14
|
+
AdaptOrchClient,
|
|
15
|
+
ClientConfig,
|
|
16
|
+
ProviderCredential,
|
|
17
|
+
validate_api_url,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from adaptorch_cli import config_store
|
|
21
|
+
from adaptorch_cli.parser import build_parser, parse_args
|
|
22
|
+
|
|
23
|
+
JSONValue: TypeAlias = bool | int | float | str | list["JSONValue"] | dict[str, "JSONValue"] | None
|
|
24
|
+
JSONMapping: TypeAlias = dict[str, JSONValue]
|
|
25
|
+
|
|
26
|
+
_MAX_SUBMIT_BYTES = 8 * 1024 * 1024
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class PayloadResult(Protocol):
|
|
30
|
+
def to_payload(self) -> JSONMapping: ...
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _write_json(payload: JSONMapping) -> None:
|
|
34
|
+
print(json.dumps(payload, allow_nan=False, sort_keys=True, separators=(",", ":")))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _reject_json_constant(_value: str) -> Never:
|
|
38
|
+
raise ValueError("non-finite JSON number")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _finite_float(text: str) -> float:
|
|
42
|
+
value = float(text)
|
|
43
|
+
if not math.isfinite(value):
|
|
44
|
+
raise ValueError("non-finite JSON number")
|
|
45
|
+
return value
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _validated_api_url(api_url: str, parser: argparse.ArgumentParser) -> str:
|
|
49
|
+
try:
|
|
50
|
+
return validate_api_url(api_url)
|
|
51
|
+
except ValueError:
|
|
52
|
+
parser.error(
|
|
53
|
+
"invalid --api-url: expected an https:// origin (or exact loopback http://)"
|
|
54
|
+
" without credentials, path, query, or fragment"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _read_submit_bytes(file_name: str, parser: argparse.ArgumentParser) -> bytes:
|
|
59
|
+
try:
|
|
60
|
+
if file_name == "-":
|
|
61
|
+
return sys.stdin.buffer.read(_MAX_SUBMIT_BYTES + 1)
|
|
62
|
+
with Path(file_name).open("rb") as stream:
|
|
63
|
+
return stream.read(_MAX_SUBMIT_BYTES + 1)
|
|
64
|
+
except OSError:
|
|
65
|
+
parser.error("submit input must be a readable JSON object")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _read_submit_payload(file_name: str, parser: argparse.ArgumentParser) -> JSONMapping:
|
|
69
|
+
raw = _read_submit_bytes(file_name, parser)
|
|
70
|
+
if len(raw) > _MAX_SUBMIT_BYTES:
|
|
71
|
+
parser.error("submit input exceeds the 8 MiB limit")
|
|
72
|
+
try:
|
|
73
|
+
value: JSONValue = json.loads(
|
|
74
|
+
raw.decode("utf-8"),
|
|
75
|
+
parse_constant=_reject_json_constant,
|
|
76
|
+
parse_float=_finite_float,
|
|
77
|
+
)
|
|
78
|
+
# pi-lens-ignore: unreachable-except
|
|
79
|
+
except RecursionError:
|
|
80
|
+
parser.error("submit input JSON is too deeply nested")
|
|
81
|
+
except ValueError:
|
|
82
|
+
parser.error("submit input must be a readable JSON object")
|
|
83
|
+
if not isinstance(value, dict):
|
|
84
|
+
parser.error("submit input must be a JSON object")
|
|
85
|
+
return value
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _require_client(api_url: str) -> AdaptOrchClient:
|
|
89
|
+
api_key, _source = config_store.resolve_api_key()
|
|
90
|
+
if not api_key:
|
|
91
|
+
print(
|
|
92
|
+
"authentication required: run `adaptorchctl auth login` or set ADAPTORCH_API_KEY",
|
|
93
|
+
file=sys.stderr,
|
|
94
|
+
)
|
|
95
|
+
raise SystemExit(3)
|
|
96
|
+
return AdaptOrchClient(ClientConfig(api_url=api_url, api_key=api_key))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _submission_credential() -> ProviderCredential | None:
|
|
100
|
+
resolved, _source = config_store.resolve_provider_credential()
|
|
101
|
+
if resolved is None:
|
|
102
|
+
return None
|
|
103
|
+
provider, model, key = resolved
|
|
104
|
+
return ProviderCredential(provider, model, key)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _read_secret(prompt: str, *, stdin_flag: bool) -> str:
|
|
108
|
+
"""Read a secret from stdin (piped or --value-stdin) or a hidden prompt.
|
|
109
|
+
|
|
110
|
+
Never echoed: values on argv are rejected upstream by _CREDENTIAL_FLAGS.
|
|
111
|
+
"""
|
|
112
|
+
if stdin_flag or not sys.stdin.isatty():
|
|
113
|
+
value = sys.stdin.read().strip()
|
|
114
|
+
else:
|
|
115
|
+
import getpass
|
|
116
|
+
|
|
117
|
+
value = getpass.getpass(prompt).strip()
|
|
118
|
+
return value
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _result_payload(result: PayloadResult) -> JSONMapping:
|
|
122
|
+
return result.to_payload()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _auth_command(
|
|
126
|
+
args: argparse.Namespace,
|
|
127
|
+
api_url: str,
|
|
128
|
+
parser: argparse.ArgumentParser,
|
|
129
|
+
) -> JSONMapping:
|
|
130
|
+
sub: str = args.auth_command
|
|
131
|
+
if sub == "status":
|
|
132
|
+
api_key, source = config_store.resolve_api_key()
|
|
133
|
+
return {
|
|
134
|
+
"authenticated": api_key is not None,
|
|
135
|
+
"credential_source": source,
|
|
136
|
+
"api_url": api_url,
|
|
137
|
+
}
|
|
138
|
+
if sub == "logout":
|
|
139
|
+
removed = config_store.clear_credentials()
|
|
140
|
+
return {"logged_out": True, "removed_stored_key": removed}
|
|
141
|
+
if sub == "login":
|
|
142
|
+
login_url = getattr(args, "login_api_url", None) or api_url
|
|
143
|
+
try:
|
|
144
|
+
login_url = validate_api_url(login_url)
|
|
145
|
+
except ValueError:
|
|
146
|
+
parser.error(
|
|
147
|
+
"invalid --api-url: expected an https:// origin (or exact loopback http://)"
|
|
148
|
+
)
|
|
149
|
+
key = _read_secret("AdaptOrch API key: ", stdin_flag=False)
|
|
150
|
+
if not key:
|
|
151
|
+
parser.error("no API key provided on stdin or at the prompt")
|
|
152
|
+
verified: bool | None = None
|
|
153
|
+
if not args.no_verify:
|
|
154
|
+
try:
|
|
155
|
+
whoami = AdaptOrchClient(ClientConfig(api_url=login_url, api_key=key)).whoami()
|
|
156
|
+
verified = True
|
|
157
|
+
_ = whoami
|
|
158
|
+
except AdaptOrchAPIError as error:
|
|
159
|
+
print(
|
|
160
|
+
f"login verification failed (HTTP {error.status_code}); "
|
|
161
|
+
"key not saved. Retry with --no-verify to store anyway.",
|
|
162
|
+
file=sys.stderr,
|
|
163
|
+
)
|
|
164
|
+
raise SystemExit(3) from error
|
|
165
|
+
config_store.store_credentials(api_key=key, api_url=login_url)
|
|
166
|
+
return {
|
|
167
|
+
"logged_in": True,
|
|
168
|
+
"api_url": login_url,
|
|
169
|
+
"verified": bool(verified),
|
|
170
|
+
"config_path": str(config_store.config_path()),
|
|
171
|
+
}
|
|
172
|
+
parser.error("unknown auth command")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _config_command(
|
|
176
|
+
args: argparse.Namespace,
|
|
177
|
+
api_url: str,
|
|
178
|
+
parser: argparse.ArgumentParser,
|
|
179
|
+
) -> JSONMapping:
|
|
180
|
+
sub: str = args.config_command
|
|
181
|
+
if sub == "get":
|
|
182
|
+
view = config_store.config_view()
|
|
183
|
+
view["api_url"] = api_url
|
|
184
|
+
return view
|
|
185
|
+
if sub == "set":
|
|
186
|
+
key: str = args.key
|
|
187
|
+
if key not in config_store.configurable_keys():
|
|
188
|
+
parser.error(
|
|
189
|
+
"unknown config key; expected one of: "
|
|
190
|
+
+ ", ".join(config_store.configurable_keys())
|
|
191
|
+
)
|
|
192
|
+
value = getattr(args, "value", None)
|
|
193
|
+
if config_store.is_secret_key(key):
|
|
194
|
+
if value is not None:
|
|
195
|
+
parser.error(f"{key} is a secret; pass it via --value-stdin or a piped stdin")
|
|
196
|
+
value = _read_secret("Value: ", stdin_flag=args.value_stdin)
|
|
197
|
+
elif args.value_stdin:
|
|
198
|
+
value = sys.stdin.read().strip()
|
|
199
|
+
elif value is None:
|
|
200
|
+
parser.error("config set requires a value (or --value-stdin)")
|
|
201
|
+
if key == "api_url":
|
|
202
|
+
try:
|
|
203
|
+
value = validate_api_url(value)
|
|
204
|
+
except ValueError:
|
|
205
|
+
parser.error(
|
|
206
|
+
"invalid api_url: expected an https:// origin (or exact loopback http://)"
|
|
207
|
+
)
|
|
208
|
+
config_store.set_config_value(key, value)
|
|
209
|
+
return {"set": key, "config_path": str(config_store.config_path())}
|
|
210
|
+
if sub == "unset":
|
|
211
|
+
removed = config_store.unset_config_value(args.key)
|
|
212
|
+
return {"unset": args.key, "removed": removed}
|
|
213
|
+
parser.error("unknown config command")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _run_command(
|
|
217
|
+
args: argparse.Namespace,
|
|
218
|
+
api_url: str,
|
|
219
|
+
parser: argparse.ArgumentParser,
|
|
220
|
+
) -> tuple[JSONMapping, bool]:
|
|
221
|
+
command: str = args.run_command
|
|
222
|
+
match command:
|
|
223
|
+
case "submit":
|
|
224
|
+
file_name: str = args.file
|
|
225
|
+
request_id: str | None = args.request_id
|
|
226
|
+
payload = _read_submit_payload(file_name, parser)
|
|
227
|
+
submit_result = _require_client(api_url).submit_run(
|
|
228
|
+
payload,
|
|
229
|
+
idempotency_key=request_id or str(uuid.uuid4()),
|
|
230
|
+
provider_credential=_submission_credential(),
|
|
231
|
+
)
|
|
232
|
+
return _result_payload(submit_result), False
|
|
233
|
+
case "list":
|
|
234
|
+
status: str | None = args.status
|
|
235
|
+
project_id: str | None = args.project_id
|
|
236
|
+
list_result = _require_client(api_url).list_runs(
|
|
237
|
+
status=status,
|
|
238
|
+
project_id=project_id,
|
|
239
|
+
)
|
|
240
|
+
return _result_payload(list_result), False
|
|
241
|
+
case "get":
|
|
242
|
+
run_id: str = args.run_id
|
|
243
|
+
return _result_payload(_require_client(api_url).get_run(run_id)), True
|
|
244
|
+
case "cancel":
|
|
245
|
+
cancel_run_id: str = args.run_id
|
|
246
|
+
reason: str | None = args.reason
|
|
247
|
+
cancel_result = _require_client(api_url).cancel_run(
|
|
248
|
+
cancel_run_id,
|
|
249
|
+
reason=reason,
|
|
250
|
+
)
|
|
251
|
+
return _result_payload(cancel_result), False
|
|
252
|
+
case _:
|
|
253
|
+
parser.error("unknown run command")
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _execute(
|
|
257
|
+
args: argparse.Namespace,
|
|
258
|
+
api_url: str,
|
|
259
|
+
parser: argparse.ArgumentParser,
|
|
260
|
+
) -> tuple[JSONMapping, bool]:
|
|
261
|
+
command: str = args.command
|
|
262
|
+
match command:
|
|
263
|
+
case "auth":
|
|
264
|
+
return _auth_command(args, api_url, parser), False
|
|
265
|
+
case "config":
|
|
266
|
+
return _config_command(args, api_url, parser), False
|
|
267
|
+
case "whoami":
|
|
268
|
+
return _result_payload(_require_client(api_url).whoami()), False
|
|
269
|
+
case "capabilities":
|
|
270
|
+
return _result_payload(_require_client(api_url).capabilities()), False
|
|
271
|
+
case "run":
|
|
272
|
+
return _run_command(args, api_url, parser)
|
|
273
|
+
case "evidence":
|
|
274
|
+
evidence_run_id: str = args.run_id
|
|
275
|
+
return _result_payload(_require_client(api_url).get_evidence(evidence_run_id)), False
|
|
276
|
+
case "artifact":
|
|
277
|
+
artifact_run_id: str = args.run_id
|
|
278
|
+
return _result_payload(_require_client(api_url).list_artifacts(artifact_run_id)), False
|
|
279
|
+
case _:
|
|
280
|
+
parser.error("unknown command")
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _api_exit_code(error: AdaptOrchAPIError) -> int:
|
|
284
|
+
status_code = error.status_code
|
|
285
|
+
if status_code in {401, 403}:
|
|
286
|
+
return 3
|
|
287
|
+
if status_code == 404:
|
|
288
|
+
return 4
|
|
289
|
+
if status_code == 409:
|
|
290
|
+
return 5
|
|
291
|
+
if status_code == 429:
|
|
292
|
+
return 6
|
|
293
|
+
if status_code is None or 500 <= status_code <= 599:
|
|
294
|
+
return 7
|
|
295
|
+
return 10
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _payload_status(payload: JSONMapping) -> str:
|
|
299
|
+
status = payload.get("status")
|
|
300
|
+
if isinstance(status, str):
|
|
301
|
+
return status
|
|
302
|
+
data = payload.get("data")
|
|
303
|
+
if isinstance(data, dict):
|
|
304
|
+
nested = data.get("status")
|
|
305
|
+
if isinstance(nested, str):
|
|
306
|
+
return nested
|
|
307
|
+
return ""
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _run_status_exit(payload: JSONMapping) -> int:
|
|
311
|
+
normalized = _payload_status(payload).lower()
|
|
312
|
+
if normalized == "failed":
|
|
313
|
+
return 8
|
|
314
|
+
if normalized == "cancelled":
|
|
315
|
+
return 9
|
|
316
|
+
if normalized == "inconclusive":
|
|
317
|
+
return 10
|
|
318
|
+
return 0
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
322
|
+
parser = build_parser()
|
|
323
|
+
args = parse_args(argv)
|
|
324
|
+
flag_url = getattr(args, "api_url", None)
|
|
325
|
+
if flag_url is not None and not flag_url.strip():
|
|
326
|
+
parser.error("invalid --api-url: expected an https:// origin (or exact loopback http://)")
|
|
327
|
+
resolved_url, _url_source = config_store.resolve_api_url(flag_url)
|
|
328
|
+
api_url = _validated_api_url(resolved_url, parser)
|
|
329
|
+
try:
|
|
330
|
+
payload, check_run_status = _execute(args, api_url, parser)
|
|
331
|
+
try:
|
|
332
|
+
_write_json(payload)
|
|
333
|
+
except ValueError:
|
|
334
|
+
print("response contained non-encodable JSON values", file=sys.stderr)
|
|
335
|
+
return 7
|
|
336
|
+
return _run_status_exit(payload) if check_run_status else 0
|
|
337
|
+
# pi-lens-ignore: unreachable-except
|
|
338
|
+
except KeyboardInterrupt:
|
|
339
|
+
print("interrupted", file=sys.stderr)
|
|
340
|
+
return 130
|
|
341
|
+
# pi-lens-ignore: unreachable-except
|
|
342
|
+
except AdaptOrchAPIError as error:
|
|
343
|
+
code = _api_exit_code(error)
|
|
344
|
+
print(f"request failed (HTTP {error.status_code})", file=sys.stderr)
|
|
345
|
+
return code
|
|
346
|
+
except ValueError:
|
|
347
|
+
parser.error("invalid CLI input")
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def entrypoint() -> None:
|
|
351
|
+
raise SystemExit(main())
|