orca-cli-sdk 0.1.0__py3-none-any.whl
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.
- orca_cli_sdk-0.1.0.dist-info/METADATA +226 -0
- orca_cli_sdk-0.1.0.dist-info/RECORD +16 -0
- orca_cli_sdk-0.1.0.dist-info/WHEEL +4 -0
- orca_cli_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
- orca_sdk/__init__.py +41 -0
- orca_sdk/_async_client.py +171 -0
- orca_sdk/_client.py +195 -0
- orca_sdk/_core.py +314 -0
- orca_sdk/_exceptions.py +105 -0
- orca_sdk/_generated.py +11094 -0
- orca_sdk/_resource.py +85 -0
- orca_sdk/_version.py +4 -0
- orca_sdk/data/agent-context.json +7811 -0
- orca_sdk/data/flag-types.json +2146 -0
- orca_sdk/py.typed +0 -0
- orca_sdk/schema.py +33 -0
orca_sdk/_core.py
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"""Transport-independent pieces shared by :class:`Orca` and :class:`AsyncOrca`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json as _json
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
from collections.abc import Mapping, Sequence
|
|
9
|
+
from dataclasses import dataclass, field, replace
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Union
|
|
12
|
+
|
|
13
|
+
from ._exceptions import (
|
|
14
|
+
OrcaBinaryNotFoundError,
|
|
15
|
+
OrcaCommandError,
|
|
16
|
+
OrcaJSONDecodeError,
|
|
17
|
+
OrcaUsageError,
|
|
18
|
+
)
|
|
19
|
+
from ._resource import FlagValue, build_args
|
|
20
|
+
|
|
21
|
+
DEFAULT_BINARY = "/home/box/Applications/bin/orca"
|
|
22
|
+
DEFAULT_TIMEOUT = 60.0
|
|
23
|
+
BINARY_ENV_VAR = "ORCA_BIN"
|
|
24
|
+
PAIRING_CODE_ENV_VAR = "ORCA_PAIRING_CODE"
|
|
25
|
+
|
|
26
|
+
PathLike = Union[str, "os.PathLike[str]"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _Unset:
|
|
30
|
+
def __repr__(self) -> str:
|
|
31
|
+
return "NOT_GIVEN"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
#: Sentinel meaning "keep the current value" in :meth:`Orca.with_options`.
|
|
35
|
+
NOT_GIVEN: Any = _Unset()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def find_binary(binary: PathLike | None = None) -> str:
|
|
39
|
+
"""Locate the ``orca`` executable.
|
|
40
|
+
|
|
41
|
+
Order: explicit ``binary`` argument, ``$ORCA_BIN``, ``orca`` on ``$PATH``,
|
|
42
|
+
then :data:`DEFAULT_BINARY`.
|
|
43
|
+
"""
|
|
44
|
+
searched: list[str] = []
|
|
45
|
+
candidates: list[str] = []
|
|
46
|
+
if binary is not None:
|
|
47
|
+
candidates.append(os.fspath(binary))
|
|
48
|
+
else:
|
|
49
|
+
env_bin = os.environ.get(BINARY_ENV_VAR)
|
|
50
|
+
if env_bin:
|
|
51
|
+
candidates.append(env_bin)
|
|
52
|
+
on_path = shutil.which("orca")
|
|
53
|
+
if on_path:
|
|
54
|
+
candidates.append(on_path)
|
|
55
|
+
else:
|
|
56
|
+
searched.append("orca on $PATH")
|
|
57
|
+
candidates.append(DEFAULT_BINARY)
|
|
58
|
+
for candidate in candidates:
|
|
59
|
+
resolved = shutil.which(candidate) if os.sep not in candidate else candidate
|
|
60
|
+
searched.append(candidate)
|
|
61
|
+
if resolved and Path(resolved).is_file() and os.access(resolved, os.X_OK):
|
|
62
|
+
return str(resolved)
|
|
63
|
+
raise OrcaBinaryNotFoundError(
|
|
64
|
+
f"Could not find the orca CLI. Set ORCA_BIN or pass binary=... (searched: {', '.join(searched)})",
|
|
65
|
+
searched,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class OrcaResponse:
|
|
71
|
+
"""A parsed CLI response.
|
|
72
|
+
|
|
73
|
+
For ``--json`` commands this mirrors the ``{id, ok, result, error, _meta}``
|
|
74
|
+
envelope. Commands without JSON output (or whose JSON is not an envelope)
|
|
75
|
+
produce ``ok=True`` and the decoded JSON or stdout text as ``result``.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
ok: bool
|
|
79
|
+
result: Any
|
|
80
|
+
argv: list[str]
|
|
81
|
+
returncode: int
|
|
82
|
+
stdout: str = ""
|
|
83
|
+
stderr: str = ""
|
|
84
|
+
error: dict[str, Any] | None = None
|
|
85
|
+
id: str | None = None
|
|
86
|
+
meta: dict[str, Any] = field(default_factory=dict)
|
|
87
|
+
raw: Any = None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass(frozen=True)
|
|
91
|
+
class ClientConfig:
|
|
92
|
+
binary: str | None
|
|
93
|
+
timeout: float | None
|
|
94
|
+
environment: str | None
|
|
95
|
+
pairing_code: str | None
|
|
96
|
+
env: Mapping[str, str] | None
|
|
97
|
+
inherit_env: bool
|
|
98
|
+
cwd: str | None
|
|
99
|
+
|
|
100
|
+
def updated(self, **changes: Any) -> ClientConfig:
|
|
101
|
+
return replace(self, **{k: v for k, v in changes.items() if v is not NOT_GIVEN})
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass(frozen=True)
|
|
105
|
+
class PreparedCall:
|
|
106
|
+
argv: list[str]
|
|
107
|
+
env: dict[str, str] | None
|
|
108
|
+
cwd: str | None
|
|
109
|
+
timeout: float | None
|
|
110
|
+
stdin: bytes | None
|
|
111
|
+
json: bool
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def make_config(
|
|
115
|
+
binary: PathLike | None,
|
|
116
|
+
timeout: float | None,
|
|
117
|
+
environment: str | None,
|
|
118
|
+
pairing_code: str | None,
|
|
119
|
+
env: Mapping[str, str] | None,
|
|
120
|
+
inherit_env: bool,
|
|
121
|
+
cwd: PathLike | None,
|
|
122
|
+
) -> ClientConfig:
|
|
123
|
+
return ClientConfig(
|
|
124
|
+
binary=os.fspath(binary) if binary is not None else None,
|
|
125
|
+
timeout=timeout,
|
|
126
|
+
environment=environment,
|
|
127
|
+
pairing_code=pairing_code,
|
|
128
|
+
env=dict(env) if env is not None else None,
|
|
129
|
+
inherit_env=inherit_env,
|
|
130
|
+
cwd=os.fspath(cwd) if cwd is not None else None,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def prepare(
|
|
135
|
+
config: ClientConfig,
|
|
136
|
+
binary: str,
|
|
137
|
+
args: Sequence[str],
|
|
138
|
+
*,
|
|
139
|
+
json: bool,
|
|
140
|
+
remote: bool,
|
|
141
|
+
stdin: str | bytes | None,
|
|
142
|
+
timeout: float | None | Any = NOT_GIVEN,
|
|
143
|
+
) -> PreparedCall:
|
|
144
|
+
argv = [binary, *args]
|
|
145
|
+
if json and "--json" not in args:
|
|
146
|
+
argv.append("--json")
|
|
147
|
+
extra_env: dict[str, str] = {}
|
|
148
|
+
if config.environment is not None or config.pairing_code is not None:
|
|
149
|
+
if not remote:
|
|
150
|
+
raise OrcaUsageError(
|
|
151
|
+
f"`orca {' '.join(args[:3])}` only runs on the local host and does not accept "
|
|
152
|
+
"--environment/--pairing-code; use client.with_options(environment=None, "
|
|
153
|
+
"pairing_code=None)"
|
|
154
|
+
)
|
|
155
|
+
if config.environment is not None:
|
|
156
|
+
argv += ["--environment", config.environment]
|
|
157
|
+
if config.pairing_code is not None:
|
|
158
|
+
# Passed via the environment so the code does not show up in `ps`.
|
|
159
|
+
extra_env[PAIRING_CODE_ENV_VAR] = config.pairing_code
|
|
160
|
+
env: dict[str, str] | None = None
|
|
161
|
+
if config.env is not None or extra_env or not config.inherit_env:
|
|
162
|
+
env = dict(os.environ) if config.inherit_env else {}
|
|
163
|
+
env.update(config.env or {})
|
|
164
|
+
env.update(extra_env)
|
|
165
|
+
data = stdin.encode() if isinstance(stdin, str) else stdin
|
|
166
|
+
return PreparedCall(
|
|
167
|
+
argv=argv,
|
|
168
|
+
env=env,
|
|
169
|
+
cwd=config.cwd,
|
|
170
|
+
timeout=config.timeout if timeout is NOT_GIVEN else timeout,
|
|
171
|
+
stdin=data,
|
|
172
|
+
json=json,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def prepare_invoke(
|
|
177
|
+
config: ClientConfig,
|
|
178
|
+
binary: str,
|
|
179
|
+
command: str,
|
|
180
|
+
positionals: Sequence[Any],
|
|
181
|
+
flags: Sequence[tuple[str, FlagValue]],
|
|
182
|
+
*,
|
|
183
|
+
json: bool,
|
|
184
|
+
remote: bool,
|
|
185
|
+
stdin: str | bytes | None = None,
|
|
186
|
+
) -> PreparedCall:
|
|
187
|
+
return prepare(
|
|
188
|
+
config,
|
|
189
|
+
binary,
|
|
190
|
+
build_args(command, positionals, flags),
|
|
191
|
+
json=json,
|
|
192
|
+
remote=remote,
|
|
193
|
+
stdin=stdin,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _decode(data: bytes | str | None) -> str:
|
|
198
|
+
if data is None:
|
|
199
|
+
return ""
|
|
200
|
+
if isinstance(data, bytes):
|
|
201
|
+
return data.decode("utf-8", errors="replace")
|
|
202
|
+
return data
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def parse_output(
|
|
206
|
+
call: PreparedCall,
|
|
207
|
+
returncode: int,
|
|
208
|
+
stdout_b: bytes | str | None,
|
|
209
|
+
stderr_b: bytes | str | None,
|
|
210
|
+
*,
|
|
211
|
+
check: bool = True,
|
|
212
|
+
) -> OrcaResponse:
|
|
213
|
+
"""Turn raw process output into an :class:`OrcaResponse`, raising on failure."""
|
|
214
|
+
stdout, stderr = _decode(stdout_b), _decode(stderr_b)
|
|
215
|
+
argv = call.argv
|
|
216
|
+
if not call.json:
|
|
217
|
+
response = OrcaResponse(
|
|
218
|
+
ok=returncode == 0,
|
|
219
|
+
result=stdout,
|
|
220
|
+
argv=argv,
|
|
221
|
+
returncode=returncode,
|
|
222
|
+
stdout=stdout,
|
|
223
|
+
stderr=stderr,
|
|
224
|
+
)
|
|
225
|
+
if check and returncode != 0:
|
|
226
|
+
raise _process_error(response)
|
|
227
|
+
return response
|
|
228
|
+
|
|
229
|
+
text = stdout.strip()
|
|
230
|
+
if not text:
|
|
231
|
+
if returncode != 0:
|
|
232
|
+
response = OrcaResponse(False, None, argv, returncode, stdout, stderr)
|
|
233
|
+
if check:
|
|
234
|
+
raise _process_error(response)
|
|
235
|
+
return response
|
|
236
|
+
raise OrcaJSONDecodeError(
|
|
237
|
+
"orca produced no output for a --json command",
|
|
238
|
+
argv=argv,
|
|
239
|
+
stdout=stdout,
|
|
240
|
+
stderr=stderr,
|
|
241
|
+
returncode=returncode,
|
|
242
|
+
)
|
|
243
|
+
try:
|
|
244
|
+
payload = _json.loads(text)
|
|
245
|
+
except ValueError as exc:
|
|
246
|
+
if returncode != 0 and check:
|
|
247
|
+
raise _process_error(OrcaResponse(False, None, argv, returncode, stdout, stderr)) from exc
|
|
248
|
+
raise OrcaJSONDecodeError(
|
|
249
|
+
f"could not decode orca --json output: {exc}",
|
|
250
|
+
argv=argv,
|
|
251
|
+
stdout=stdout,
|
|
252
|
+
stderr=stderr,
|
|
253
|
+
returncode=returncode,
|
|
254
|
+
) from exc
|
|
255
|
+
|
|
256
|
+
if isinstance(payload, dict) and isinstance(payload.get("ok"), bool):
|
|
257
|
+
error = payload.get("error")
|
|
258
|
+
response = OrcaResponse(
|
|
259
|
+
ok=payload["ok"] and returncode == 0,
|
|
260
|
+
result=payload.get("result"),
|
|
261
|
+
argv=argv,
|
|
262
|
+
returncode=returncode,
|
|
263
|
+
stdout=stdout,
|
|
264
|
+
stderr=stderr,
|
|
265
|
+
error=error if isinstance(error, dict) else None,
|
|
266
|
+
id=payload.get("id"),
|
|
267
|
+
meta=payload.get("_meta") or {},
|
|
268
|
+
raw=payload,
|
|
269
|
+
)
|
|
270
|
+
else:
|
|
271
|
+
# A few commands (e.g. `skills get`) print bare JSON rather than an envelope.
|
|
272
|
+
response = OrcaResponse(
|
|
273
|
+
ok=returncode == 0,
|
|
274
|
+
result=payload,
|
|
275
|
+
argv=argv,
|
|
276
|
+
returncode=returncode,
|
|
277
|
+
stdout=stdout,
|
|
278
|
+
stderr=stderr,
|
|
279
|
+
raw=payload,
|
|
280
|
+
)
|
|
281
|
+
if check and not response.ok:
|
|
282
|
+
raise _command_error(response)
|
|
283
|
+
return response
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _command_error(response: OrcaResponse) -> OrcaCommandError:
|
|
287
|
+
if response.error is None:
|
|
288
|
+
return _process_error(response)
|
|
289
|
+
return OrcaCommandError(
|
|
290
|
+
str(response.error.get("code") or "unknown_error"),
|
|
291
|
+
str(response.error.get("message") or ""),
|
|
292
|
+
data=response.error.get("data"),
|
|
293
|
+
argv=response.argv,
|
|
294
|
+
returncode=response.returncode,
|
|
295
|
+
stdout=response.stdout,
|
|
296
|
+
stderr=response.stderr,
|
|
297
|
+
request_id=response.id,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _process_error(response: OrcaResponse) -> OrcaCommandError:
|
|
302
|
+
message = (
|
|
303
|
+
response.stderr.strip()
|
|
304
|
+
or response.stdout.strip()
|
|
305
|
+
or (f"orca exited with status {response.returncode}")
|
|
306
|
+
)
|
|
307
|
+
return OrcaCommandError(
|
|
308
|
+
"process_error",
|
|
309
|
+
message,
|
|
310
|
+
argv=response.argv,
|
|
311
|
+
returncode=response.returncode,
|
|
312
|
+
stdout=response.stdout,
|
|
313
|
+
stderr=response.stderr,
|
|
314
|
+
)
|
orca_sdk/_exceptions.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Exception hierarchy for the Orca SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"OrcaBinaryNotFoundError",
|
|
10
|
+
"OrcaCommandError",
|
|
11
|
+
"OrcaError",
|
|
12
|
+
"OrcaJSONDecodeError",
|
|
13
|
+
"OrcaTimeoutError",
|
|
14
|
+
"OrcaUsageError",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class OrcaError(Exception):
|
|
19
|
+
"""Base class for every error raised by the SDK."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class OrcaBinaryNotFoundError(OrcaError, FileNotFoundError):
|
|
23
|
+
"""The ``orca`` executable could not be located."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, message: str, searched: Sequence[str] = ()) -> None:
|
|
26
|
+
super().__init__(message)
|
|
27
|
+
self.searched = list(searched)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class OrcaUsageError(OrcaError, ValueError):
|
|
31
|
+
"""The SDK was asked to build a command the CLI would reject."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class OrcaCommandError(OrcaError):
|
|
35
|
+
"""The CLI ran but reported failure.
|
|
36
|
+
|
|
37
|
+
``code`` and ``message`` come from the JSON error envelope when there is
|
|
38
|
+
one (e.g. ``repo_not_found``); otherwise ``code`` is ``"process_error"``
|
|
39
|
+
and ``message`` is taken from stderr/stdout.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
code: str,
|
|
45
|
+
message: str,
|
|
46
|
+
*,
|
|
47
|
+
data: Any = None,
|
|
48
|
+
argv: Sequence[str] = (),
|
|
49
|
+
returncode: int | None = None,
|
|
50
|
+
stdout: str = "",
|
|
51
|
+
stderr: str = "",
|
|
52
|
+
request_id: str | None = None,
|
|
53
|
+
) -> None:
|
|
54
|
+
super().__init__(f"[{code}] {message}")
|
|
55
|
+
self.code = code
|
|
56
|
+
self.message = message
|
|
57
|
+
self.data = data
|
|
58
|
+
self.argv = list(argv)
|
|
59
|
+
self.returncode = returncode
|
|
60
|
+
self.stdout = stdout
|
|
61
|
+
self.stderr = stderr
|
|
62
|
+
self.request_id = request_id
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def next_steps(self) -> list[str]:
|
|
66
|
+
"""Hints the CLI attached under ``error.data.nextSteps``, if any."""
|
|
67
|
+
if isinstance(self.data, dict) and isinstance(self.data.get("nextSteps"), list):
|
|
68
|
+
return [str(s) for s in self.data["nextSteps"]]
|
|
69
|
+
return []
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class OrcaTimeoutError(OrcaError, TimeoutError):
|
|
73
|
+
"""The CLI did not finish within the configured timeout and was killed."""
|
|
74
|
+
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
timeout: float,
|
|
78
|
+
argv: Sequence[str] = (),
|
|
79
|
+
stdout: str = "",
|
|
80
|
+
stderr: str = "",
|
|
81
|
+
) -> None:
|
|
82
|
+
super().__init__(f"orca command timed out after {timeout:g}s: {' '.join(argv)}")
|
|
83
|
+
self.timeout = timeout
|
|
84
|
+
self.argv = list(argv)
|
|
85
|
+
self.stdout = stdout
|
|
86
|
+
self.stderr = stderr
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class OrcaJSONDecodeError(OrcaError, ValueError):
|
|
90
|
+
"""``--json`` output could not be parsed as JSON."""
|
|
91
|
+
|
|
92
|
+
def __init__(
|
|
93
|
+
self,
|
|
94
|
+
message: str,
|
|
95
|
+
*,
|
|
96
|
+
argv: Sequence[str] = (),
|
|
97
|
+
stdout: str = "",
|
|
98
|
+
stderr: str = "",
|
|
99
|
+
returncode: int | None = None,
|
|
100
|
+
) -> None:
|
|
101
|
+
super().__init__(message)
|
|
102
|
+
self.argv = list(argv)
|
|
103
|
+
self.stdout = stdout
|
|
104
|
+
self.stderr = stderr
|
|
105
|
+
self.returncode = returncode
|