code-analysis-client-dev 1.6.341__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.
- code_analysis_client/__init__.py +142 -0
- code_analysis_client/check_chain.py +374 -0
- code_analysis_client/client.py +728 -0
- code_analysis_client/commands_proxy.py +63 -0
- code_analysis_client/config.py +198 -0
- code_analysis_client/exceptions.py +188 -0
- code_analysis_client/file_session.py +823 -0
- code_analysis_client/project.py +59 -0
- code_analysis_client/py.typed +0 -0
- code_analysis_client/queue_wait.py +341 -0
- code_analysis_client/responses.py +126 -0
- code_analysis_client/schema_disk_cache.py +207 -0
- code_analysis_client/server_api.py +291 -0
- code_analysis_client/server_schema.py +57 -0
- code_analysis_client/symbol_tree.py +13 -0
- code_analysis_client/universal_file.py +50 -0
- code_analysis_client/validation.py +98 -0
- code_analysis_client/vectorizer_state.py +159 -0
- code_analysis_client/version.txt +1 -0
- code_analysis_client/worktree.py +533 -0
- code_analysis_client_dev-1.6.341.dist-info/METADATA +251 -0
- code_analysis_client_dev-1.6.341.dist-info/RECORD +24 -0
- code_analysis_client_dev-1.6.341.dist-info/WHEEL +5 -0
- code_analysis_client_dev-1.6.341.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project-level typed methods on top of CodeAnalysisAsyncClient.
|
|
3
|
+
|
|
4
|
+
Wraps ``project_documentation_server_override`` (per-project address override of the documentation
|
|
5
|
+
server, used by live proofs that substitute an unreachable address).
|
|
6
|
+
|
|
7
|
+
Author: Vasiliy Zdanovskiy
|
|
8
|
+
email: vasilyvz@gmail.com
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import TYPE_CHECKING, Any, Dict, Optional
|
|
14
|
+
|
|
15
|
+
from code_analysis_client.exceptions import ClientValidationError
|
|
16
|
+
from code_analysis_client.responses import FacadeCaller, unwrap_command_result
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from code_analysis_client.client import CodeAnalysisAsyncClient
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ProjectClient:
|
|
23
|
+
"""Project-scoped commands that are not part of the session workflow."""
|
|
24
|
+
|
|
25
|
+
__slots__ = ("_client",)
|
|
26
|
+
|
|
27
|
+
def __init__(self, client: CodeAnalysisAsyncClient) -> None:
|
|
28
|
+
"""Store the underlying async client."""
|
|
29
|
+
self._client = FacadeCaller(client) # type: ignore[assignment]
|
|
30
|
+
|
|
31
|
+
async def documentation_server_override(
|
|
32
|
+
self,
|
|
33
|
+
project_id: str,
|
|
34
|
+
*,
|
|
35
|
+
host: Optional[str] = None,
|
|
36
|
+
port: Optional[int] = None,
|
|
37
|
+
) -> Dict[str, Any]:
|
|
38
|
+
"""Set (``host`` and ``port``) or clear (neither) the documentation-server override.
|
|
39
|
+
|
|
40
|
+
Calls ``project_documentation_server_override``; returns ``project_id``, ``override``
|
|
41
|
+
(``None`` or ``{host, port}``) and ``effective`` (``None`` or ``{protocol, host, port}``).
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
ClientValidationError: ``host`` and ``port`` were not supplied together.
|
|
45
|
+
"""
|
|
46
|
+
if (host is None) != (port is None):
|
|
47
|
+
raise ClientValidationError(
|
|
48
|
+
"host and port must be supplied together or both omitted",
|
|
49
|
+
field="port" if host is not None else "host",
|
|
50
|
+
)
|
|
51
|
+
params: Dict[str, Any] = {"project_id": project_id}
|
|
52
|
+
if host is not None:
|
|
53
|
+
params["host"] = host
|
|
54
|
+
params["port"] = port
|
|
55
|
+
return unwrap_command_result(
|
|
56
|
+
await self._client.call_validated(
|
|
57
|
+
"project_documentation_server_override", params
|
|
58
|
+
)
|
|
59
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Queued-job handle and the single place that maps adapter results/errors onto client contracts.
|
|
3
|
+
|
|
4
|
+
Waiting itself is NOT done here: every wait is the adapter's standard call
|
|
5
|
+
(``JsonRpcClient.execute_command_unified`` for a new command, ``JsonRpcClient.wait_for_job_via_websocket``
|
|
6
|
+
for an existing job id). This module only turns what the adapter returns or raises into the
|
|
7
|
+
client's public result/exception contract:
|
|
8
|
+
|
|
9
|
+
* inner ``success=false`` -> :class:`~code_analysis_client.exceptions.CommandFailedError`
|
|
10
|
+
* job failed (the command's own error) -> :class:`~code_analysis_client.exceptions.CommandFailedError`
|
|
11
|
+
* job stopped / cancelled -> :class:`~code_analysis_client.exceptions.JobFailedError`
|
|
12
|
+
* caller wait timeout (job keeps running) -> :class:`~code_analysis_client.exceptions.JobTimeoutError`
|
|
13
|
+
* server JSON-RPC refusal -> :class:`~code_analysis_client.exceptions.ClientValidationError`
|
|
14
|
+
(mapped in ``client.py`` next to the transport call)
|
|
15
|
+
|
|
16
|
+
Author: Vasiliy Zdanovskiy
|
|
17
|
+
email: vasilyvz@gmail.com
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import asyncio
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from typing import Any, Awaitable, Callable, Dict, Optional, Union
|
|
25
|
+
|
|
26
|
+
from mcp_proxy_adapter.client.jsonrpc_client.exceptions import (
|
|
27
|
+
ClientRemoteResponseError,
|
|
28
|
+
CommandInnerFailureError,
|
|
29
|
+
CommandSessionFailedError,
|
|
30
|
+
CommandSessionTimeoutError,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
from code_analysis_client.exceptions import (
|
|
34
|
+
ClientValidationError,
|
|
35
|
+
CommandFailedError,
|
|
36
|
+
JobFailedError,
|
|
37
|
+
JobTimeoutError,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
_FAILED_JOB_STATUSES = frozenset({"failed", "stopped", "cancelled"})
|
|
41
|
+
_TERMINAL_JOB_STATUSES = frozenset({"completed"}) | _FAILED_JOB_STATUSES
|
|
42
|
+
|
|
43
|
+
#: Terminal command-session event name -> job status, used when the status payload has none.
|
|
44
|
+
_EVENT_STATUS = {
|
|
45
|
+
"command_failed": "failed",
|
|
46
|
+
"command_error": "failed",
|
|
47
|
+
"command_cancelled": "cancelled",
|
|
48
|
+
"job_failed": "failed",
|
|
49
|
+
"job_stopped": "stopped",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
StatusHook = Callable[[Dict[str, Any]], Union[Any, Awaitable[Any]]]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def check_wait_arguments(timeout: Optional[float], poll_interval: Optional[float]) -> None:
|
|
56
|
+
"""Reject a negative ``timeout`` and a non-positive ``poll_interval`` before anything is sent."""
|
|
57
|
+
if timeout is not None and (isinstance(timeout, bool) or timeout < 0):
|
|
58
|
+
raise ValueError(f"timeout must be >= 0 or None, got {timeout!r}")
|
|
59
|
+
if poll_interval is not None and (isinstance(poll_interval, bool) or poll_interval <= 0):
|
|
60
|
+
raise ValueError(f"poll_interval must be > 0 or None, got {poll_interval!r}")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def inner_command_result(value: Any) -> Dict[str, Any]:
|
|
64
|
+
"""Return the command's own result envelope from a queued-job result value.
|
|
65
|
+
|
|
66
|
+
The queue stores ``{"job_id", "command", "result": <command envelope>, "status"}``; a value
|
|
67
|
+
without that wrapper is taken as the envelope itself. A non-dict result is wrapped as
|
|
68
|
+
``{"result": value}`` so callers always get a dict.
|
|
69
|
+
"""
|
|
70
|
+
inner = value["result"] if isinstance(value, dict) and "result" in value else value
|
|
71
|
+
return inner if isinstance(inner, dict) else {"result": inner}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _extract_inner_command_error(status_data: Dict[str, Any]) -> Any:
|
|
75
|
+
"""Pull the failed command's own structured error out of a job status dict.
|
|
76
|
+
|
|
77
|
+
Looks at ``status_data["result"]["result"]["error"]`` first (outer ``result`` is the queue
|
|
78
|
+
wrapper, inner ``result`` is the command's own envelope), then ``status_data["result"]["error"]``.
|
|
79
|
+
"""
|
|
80
|
+
result_field = status_data.get("result")
|
|
81
|
+
if not isinstance(result_field, dict):
|
|
82
|
+
return None
|
|
83
|
+
inner = result_field.get("result")
|
|
84
|
+
if isinstance(inner, dict) and inner.get("error") is not None:
|
|
85
|
+
return inner["error"]
|
|
86
|
+
if result_field.get("error") is not None:
|
|
87
|
+
return result_field["error"]
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def remote_error_text(message: Any, code: Any) -> str:
|
|
92
|
+
"""Text of a server JSON-RPC error, identical to the adapter's ``ClientRemoteResponseError``.
|
|
93
|
+
|
|
94
|
+
``"JSON-RPC error: <message> (code: <code>)"`` - used for an error carried by a failed job so
|
|
95
|
+
that it reads exactly like the same error answered at the top level.
|
|
96
|
+
"""
|
|
97
|
+
return f"JSON-RPC error: {message} (code: {code})"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def invalid_params_error(
|
|
101
|
+
message: str, data: Any, *, code: str | int = -32602
|
|
102
|
+
) -> ClientValidationError:
|
|
103
|
+
"""Build the :class:`ClientValidationError` for a server refusal of a command's parameters.
|
|
104
|
+
|
|
105
|
+
The single constructor for the top-level -32602 refusal (``client.py``), a -32602 carried by a
|
|
106
|
+
failed job (``job_failure``) and a refused command-session open with the server's own error
|
|
107
|
+
code (``map_adapter_error``), so ``code`` / ``field`` / ``details`` have one shape: ``details``
|
|
108
|
+
is the server's error ``data`` object and ``field`` its ``field`` entry (else ``"command"``).
|
|
109
|
+
Callers select the public code according to the command contract.
|
|
110
|
+
"""
|
|
111
|
+
payload = data if isinstance(data, dict) else {}
|
|
112
|
+
field = payload.get("field")
|
|
113
|
+
return ClientValidationError(
|
|
114
|
+
message,
|
|
115
|
+
field=field if isinstance(field, str) else "command",
|
|
116
|
+
details=payload,
|
|
117
|
+
code=code,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def validation_error_code_for_command(command: Optional[str]) -> str | int:
|
|
122
|
+
"""Return the command's public code for a remote invalid-parameter response."""
|
|
123
|
+
return -32602 if command in (None, "symbol_tree") else "VALIDATION_ERROR"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def command_failed(
|
|
127
|
+
command: Optional[str], job_id: Optional[str], inner: Dict[str, Any]
|
|
128
|
+
) -> CommandFailedError:
|
|
129
|
+
"""Build the :class:`CommandFailedError` for an inner envelope with ``success`` false."""
|
|
130
|
+
error = inner.get("error")
|
|
131
|
+
if error is None:
|
|
132
|
+
error = {k: inner[k] for k in ("code", "message") if k in inner} or inner
|
|
133
|
+
return CommandFailedError(command, job_id, error, result=inner)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def job_failure(
|
|
137
|
+
job_id: Optional[str],
|
|
138
|
+
status_data: Optional[Dict[str, Any]],
|
|
139
|
+
terminal_event: Optional[Dict[str, Any]] = None,
|
|
140
|
+
*,
|
|
141
|
+
command: Optional[str] = None,
|
|
142
|
+
) -> Union[CommandFailedError, JobFailedError, ClientValidationError]:
|
|
143
|
+
"""Build the error for a job that ended failed / stopped / cancelled.
|
|
144
|
+
|
|
145
|
+
A job whose structured error carries JSON-RPC ``-32602`` (the server validates after
|
|
146
|
+
enqueueing) yields :class:`ClientValidationError` exactly like a top-level refusal. A job that
|
|
147
|
+
ended ``failed`` (its command returned an error, or the job itself failed) yields
|
|
148
|
+
:class:`CommandFailedError` carrying the structured error, the same as an immediate command
|
|
149
|
+
that returns an error envelope (owner decision 2026-11-07, bug e58c5982, contract RC7). Only a
|
|
150
|
+
``stopped`` / ``cancelled`` job yields :class:`JobFailedError`.
|
|
151
|
+
|
|
152
|
+
``error`` is always the structured error object: the failed command's own error when the
|
|
153
|
+
status payload carries one, else the queue-level error (a bare string is wrapped as
|
|
154
|
+
``{"message": ...}``), else ``{"message": "job <status>"}``.
|
|
155
|
+
"""
|
|
156
|
+
status_data = status_data if isinstance(status_data, dict) else {}
|
|
157
|
+
terminal_event = terminal_event if isinstance(terminal_event, dict) else {}
|
|
158
|
+
status = str(status_data.get("status") or "").strip().lower()
|
|
159
|
+
if status not in _FAILED_JOB_STATUSES:
|
|
160
|
+
status = _EVENT_STATUS.get(str(terminal_event.get("event") or ""), status or "failed")
|
|
161
|
+
error = _extract_inner_command_error(status_data)
|
|
162
|
+
if error is None:
|
|
163
|
+
error = status_data.get("error")
|
|
164
|
+
if error is None:
|
|
165
|
+
error = terminal_event.get("error")
|
|
166
|
+
if not isinstance(error, dict):
|
|
167
|
+
error = {"message": str(error) if error else f"job {status}", "status": status}
|
|
168
|
+
if error.get("code") == -32602:
|
|
169
|
+
return invalid_params_error(
|
|
170
|
+
remote_error_text(error.get("message"), -32602),
|
|
171
|
+
error.get("data"),
|
|
172
|
+
code=validation_error_code_for_command(command),
|
|
173
|
+
)
|
|
174
|
+
if status == "failed":
|
|
175
|
+
return CommandFailedError(command, job_id, error)
|
|
176
|
+
return JobFailedError(job_id, error, status=status)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def result_from_job_status(
|
|
180
|
+
command: Optional[str], job_id: str, status_data: Dict[str, Any]
|
|
181
|
+
) -> Dict[str, Any]:
|
|
182
|
+
"""Turn a terminal job status payload into the call result, or raise the matching error."""
|
|
183
|
+
status = str(status_data.get("status") or "").strip().lower()
|
|
184
|
+
if status in _FAILED_JOB_STATUSES or status_data.get("error"):
|
|
185
|
+
raise job_failure(job_id, status_data, command=command)
|
|
186
|
+
inner = inner_command_result(status_data.get("result"))
|
|
187
|
+
flagged = (
|
|
188
|
+
status_data.get("command_success") is False
|
|
189
|
+
or bool(status_data.get("completed_with_error"))
|
|
190
|
+
or bool(status_data.get("native_completed_with_error"))
|
|
191
|
+
)
|
|
192
|
+
if inner.get("success") is False or flagged:
|
|
193
|
+
raise command_failed(command, job_id, inner)
|
|
194
|
+
return inner
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def result_from_unified(
|
|
198
|
+
command: str, envelope: Dict[str, Any]
|
|
199
|
+
) -> Dict[str, Any]:
|
|
200
|
+
"""Return the command result from an ``execute_command_unified`` envelope.
|
|
201
|
+
|
|
202
|
+
An inner ``success=false`` raises :class:`CommandFailedError` (it never reaches the caller as
|
|
203
|
+
a dict), for the immediate and the queued mode alike.
|
|
204
|
+
"""
|
|
205
|
+
value = envelope.get("result")
|
|
206
|
+
inner = value if envelope.get("mode") == "immediate" else inner_command_result(value)
|
|
207
|
+
if not isinstance(inner, dict):
|
|
208
|
+
inner = {"result": inner}
|
|
209
|
+
if inner.get("success") is False:
|
|
210
|
+
raise command_failed(command, envelope.get("job_id"), inner)
|
|
211
|
+
return inner
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def map_adapter_error(
|
|
215
|
+
exc: BaseException, command: str, timeout: Optional[float]
|
|
216
|
+
) -> Optional[BaseException]:
|
|
217
|
+
"""Map an adapter exception onto the client's public exception, or ``None`` to re-raise as is.
|
|
218
|
+
|
|
219
|
+
* ``CommandSessionTimeoutError`` / ``asyncio.TimeoutError`` -> ``JobTimeoutError`` (the job keeps running)
|
|
220
|
+
* ``CommandInnerFailureError`` -> ``CommandFailedError``
|
|
221
|
+
* ``CommandSessionFailedError`` with a terminal event -> ``CommandFailedError`` when the job
|
|
222
|
+
``failed``, ``JobFailedError`` when it was stopped / cancelled (structured error)
|
|
223
|
+
* ``CommandSessionFailedError`` for a refused open (numeric ``code``) -> ``ClientRemoteResponseError``
|
|
224
|
+
so the caller's JSON-RPC handling (-32602, -32601) is the same as on the HTTP path
|
|
225
|
+
* ``CommandSessionFailedError`` for a refused open carrying the server's own string error
|
|
226
|
+
``code`` (the adapter forwards ``exc.code`` of the refusing exception, e.g. ``VALIDATION_ERROR``
|
|
227
|
+
from a command's ``validate_params`` before enqueue) -> ``ClientValidationError`` with that
|
|
228
|
+
exact ``code``, the server message and ``details`` = the server's ``data`` (the documented
|
|
229
|
+
refusal shape of :class:`ClientValidationError`)
|
|
230
|
+
"""
|
|
231
|
+
if isinstance(exc, CommandSessionTimeoutError):
|
|
232
|
+
details = exc.details if isinstance(exc.details, dict) else {}
|
|
233
|
+
return JobTimeoutError(details.get("job_id"), details.get("timeout", timeout))
|
|
234
|
+
if isinstance(exc, asyncio.TimeoutError):
|
|
235
|
+
return JobTimeoutError(None, timeout)
|
|
236
|
+
if isinstance(exc, CommandInnerFailureError):
|
|
237
|
+
inner = exc.result if isinstance(exc.result, dict) else {}
|
|
238
|
+
if not isinstance(inner.get("error"), dict):
|
|
239
|
+
inner = {
|
|
240
|
+
"success": False,
|
|
241
|
+
"error": {
|
|
242
|
+
"code": exc.error_code,
|
|
243
|
+
"message": exc.error_message,
|
|
244
|
+
"data": exc.error_data,
|
|
245
|
+
},
|
|
246
|
+
}
|
|
247
|
+
return command_failed(exc.command, exc.job_id, inner)
|
|
248
|
+
if isinstance(exc, CommandSessionFailedError):
|
|
249
|
+
details = exc.details if isinstance(exc.details, dict) else {}
|
|
250
|
+
terminal = details.get("terminal_event")
|
|
251
|
+
if isinstance(terminal, dict):
|
|
252
|
+
job_id = terminal.get("job_id")
|
|
253
|
+
status_data = details.get("result_status")
|
|
254
|
+
if job_id is None and isinstance(status_data, dict):
|
|
255
|
+
job_id = status_data.get("job_id")
|
|
256
|
+
return job_failure(job_id, status_data, terminal, command=command)
|
|
257
|
+
code = details.get("code")
|
|
258
|
+
if isinstance(code, int) and not isinstance(code, bool):
|
|
259
|
+
return ClientRemoteResponseError(
|
|
260
|
+
f"JSON-RPC error: {exc} (code: {code})",
|
|
261
|
+
operation="command_session.open",
|
|
262
|
+
details={
|
|
263
|
+
"code": code,
|
|
264
|
+
"data": details.get("data"),
|
|
265
|
+
"command": command,
|
|
266
|
+
},
|
|
267
|
+
)
|
|
268
|
+
if isinstance(code, str) and code.strip():
|
|
269
|
+
return invalid_params_error(
|
|
270
|
+
str(details.get("error") or exc),
|
|
271
|
+
details.get("data"),
|
|
272
|
+
code=code.strip(),
|
|
273
|
+
)
|
|
274
|
+
return None
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
@dataclass
|
|
278
|
+
class QueuedJob:
|
|
279
|
+
"""Handle for a queued job returned when ``auto_poll=False`` detaches from the job.
|
|
280
|
+
|
|
281
|
+
Returned by ``CodeAnalysisAsyncClient`` calls made with ``auto_poll=False`` (for every
|
|
282
|
+
command, queued or formerly immediate). ``job_id`` is a non-empty string and ``envelope`` the
|
|
283
|
+
adapter's acceptance envelope. :meth:`wait` blocks on the job and returns/raises exactly like
|
|
284
|
+
the default (auto-polling) path.
|
|
285
|
+
"""
|
|
286
|
+
|
|
287
|
+
job_id: str
|
|
288
|
+
envelope: Dict[str, Any]
|
|
289
|
+
rpc: Any = field(repr=False)
|
|
290
|
+
#: Command name, used in :class:`CommandFailedError`.
|
|
291
|
+
command: Optional[str] = None
|
|
292
|
+
#: Result already produced when the adapter ran the command without a job (WS unreachable).
|
|
293
|
+
resolved: Optional[Dict[str, Any]] = field(default=None, repr=False)
|
|
294
|
+
|
|
295
|
+
async def wait(
|
|
296
|
+
self,
|
|
297
|
+
*,
|
|
298
|
+
timeout: Optional[float] = None,
|
|
299
|
+
poll_interval: Optional[float] = None,
|
|
300
|
+
status_hook: Optional[StatusHook] = None,
|
|
301
|
+
) -> Dict[str, Any]:
|
|
302
|
+
"""Wait for the job and return the command result, like the default call path.
|
|
303
|
+
|
|
304
|
+
Uses only public adapter API: one status fetch
|
|
305
|
+
(``fetch_command_session_result``) and, when the job is still running, the adapter's
|
|
306
|
+
WebSocket wait (``wait_for_job_via_websocket``) followed by one result fetch. Raises
|
|
307
|
+
:class:`JobTimeoutError` when ``timeout`` expires (the job keeps running),
|
|
308
|
+
:class:`JobFailedError` / :class:`CommandFailedError` like a direct call.
|
|
309
|
+
|
|
310
|
+
``timeout=None`` resolves to the client config ``websocket.job_wait_timeout`` (0 = no
|
|
311
|
+
bound). ``poll_interval`` is accepted for signature symmetry with the call path; the
|
|
312
|
+
public WebSocket wait for an existing job id has no poll cadence and no RPC-polling
|
|
313
|
+
fallback, so it is not used here.
|
|
314
|
+
"""
|
|
315
|
+
check_wait_arguments(timeout, poll_interval)
|
|
316
|
+
if self.resolved is not None:
|
|
317
|
+
return self.resolved
|
|
318
|
+
status_data = await self.rpc.fetch_command_session_result(self.job_id)
|
|
319
|
+
if str(status_data.get("status") or "").strip().lower() not in _TERMINAL_JOB_STATUSES:
|
|
320
|
+
try:
|
|
321
|
+
await self.rpc.wait_for_job_via_websocket(
|
|
322
|
+
self.job_id, timeout=timeout, on_inbound_message=status_hook
|
|
323
|
+
)
|
|
324
|
+
except (CommandSessionTimeoutError, asyncio.TimeoutError) as exc:
|
|
325
|
+
mapped = map_adapter_error(exc, self.command or "", timeout)
|
|
326
|
+
assert mapped is not None
|
|
327
|
+
if isinstance(mapped, JobTimeoutError) and mapped.job_id is None:
|
|
328
|
+
mapped = JobTimeoutError(self.job_id, timeout)
|
|
329
|
+
raise mapped from exc
|
|
330
|
+
status_data = await self.rpc.fetch_command_session_result(self.job_id)
|
|
331
|
+
return result_from_job_status(self.command, self.job_id, status_data)
|
|
332
|
+
|
|
333
|
+
async def status(self) -> Dict[str, Any]:
|
|
334
|
+
"""One-shot ``queue_get_job_status`` fetch (no waiting)."""
|
|
335
|
+
resp = await self.rpc.execute_command(
|
|
336
|
+
"queue_get_job_status", {"job_id": self.job_id}
|
|
337
|
+
)
|
|
338
|
+
data = resp.get("data") if isinstance(resp, dict) else None
|
|
339
|
+
if not isinstance(data, dict):
|
|
340
|
+
data = resp if isinstance(resp, dict) else {}
|
|
341
|
+
return data
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Parse JSON-RPC command envelopes from code-analysis-server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict
|
|
6
|
+
|
|
7
|
+
from code_analysis_client.exceptions import (
|
|
8
|
+
ClientValidationError,
|
|
9
|
+
CommandFailedError,
|
|
10
|
+
VectorizerUnavailableError,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def command_error_code(data: Dict[str, Any]) -> str:
|
|
15
|
+
"""Return domain or JSON-RPC error code from a failed command response."""
|
|
16
|
+
if data.get("success") is True:
|
|
17
|
+
return ""
|
|
18
|
+
top = data.get("code")
|
|
19
|
+
if top is not None and str(top).strip():
|
|
20
|
+
return str(top).strip()
|
|
21
|
+
err = data.get("error")
|
|
22
|
+
if isinstance(err, dict):
|
|
23
|
+
nested = err.get("code")
|
|
24
|
+
if nested is not None:
|
|
25
|
+
return str(nested).strip()
|
|
26
|
+
if isinstance(err, str) and err.strip():
|
|
27
|
+
return err.strip()
|
|
28
|
+
return ""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def vectorizer_error_details(data: Dict[str, Any]) -> Dict[str, Any]:
|
|
32
|
+
"""Return the ``details`` object of a failed command response.
|
|
33
|
+
|
|
34
|
+
The server (mcp-proxy-adapter ``ErrorResult``) puts the details at ``error.data``; a flat
|
|
35
|
+
envelope may carry ``details`` at the top level or ``error.details``. All three are read, in
|
|
36
|
+
the order ``error.data``, ``error.details``, top-level ``details``.
|
|
37
|
+
"""
|
|
38
|
+
err = data.get("error")
|
|
39
|
+
if isinstance(err, dict):
|
|
40
|
+
for key in ("data", "details"):
|
|
41
|
+
nested = err.get(key)
|
|
42
|
+
if isinstance(nested, dict):
|
|
43
|
+
return nested
|
|
44
|
+
top = data.get("details")
|
|
45
|
+
return top if isinstance(top, dict) else {}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def unwrap_command_result(
|
|
49
|
+
data: Dict[str, Any],
|
|
50
|
+
*,
|
|
51
|
+
session_not_found_type: type[ClientValidationError] | None = None,
|
|
52
|
+
) -> Dict[str, Any]:
|
|
53
|
+
"""Return success ``data`` dict or raise :class:`ClientValidationError`.
|
|
54
|
+
|
|
55
|
+
A refusal raises with ``.code`` equal to the server error code and ``.details`` equal to the
|
|
56
|
+
server's error details object (``{}`` when it carries none).
|
|
57
|
+
"""
|
|
58
|
+
if data.get("success"):
|
|
59
|
+
inner = data.get("data")
|
|
60
|
+
return inner if isinstance(inner, dict) else data
|
|
61
|
+
code = command_error_code(data)
|
|
62
|
+
message = data.get("message")
|
|
63
|
+
err = data.get("error")
|
|
64
|
+
if message is None and isinstance(err, dict):
|
|
65
|
+
message = err.get("message")
|
|
66
|
+
if message is None:
|
|
67
|
+
message = str(data)
|
|
68
|
+
server_details = vectorizer_error_details(data)
|
|
69
|
+
if code == "SESSION_NOT_FOUND" and session_not_found_type is not None:
|
|
70
|
+
raise session_not_found_type(
|
|
71
|
+
message, field="session_id", details=server_details, code=code
|
|
72
|
+
)
|
|
73
|
+
if code == "VECTORIZER_UNAVAILABLE":
|
|
74
|
+
details = vectorizer_error_details(data)
|
|
75
|
+
raise VectorizerUnavailableError(
|
|
76
|
+
message,
|
|
77
|
+
service=details.get("service"),
|
|
78
|
+
services=details.get("services", []),
|
|
79
|
+
reason=details.get("reason"),
|
|
80
|
+
since=details.get("since"),
|
|
81
|
+
last_probe_at=details.get("last_probe_at"),
|
|
82
|
+
retry_after_seconds=details.get("retry_after_seconds"),
|
|
83
|
+
details=details,
|
|
84
|
+
)
|
|
85
|
+
raise ClientValidationError(
|
|
86
|
+
message, field="command", details=server_details, code=code or None
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class FacadeCaller:
|
|
91
|
+
"""Client view handed to the typed facades (sessions, worktrees, check chains, previews).
|
|
92
|
+
|
|
93
|
+
The facades map a server refusal (``SESSION_NOT_FOUND``, ``VECTORIZER_UNAVAILABLE``,
|
|
94
|
+
``WORKTREE_SYNC_CONFLICT``, ...) from the failed inner envelope. The public ``call`` /
|
|
95
|
+
``call_validated`` raise :class:`CommandFailedError` for such an envelope; this view hands the
|
|
96
|
+
envelope (``CommandFailedError.result``) back to the facade unchanged so its typed errors stay
|
|
97
|
+
exactly as they were.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
__slots__ = ("_client",)
|
|
101
|
+
|
|
102
|
+
def __init__(self, client: Any) -> None:
|
|
103
|
+
"""Wrap ``client``."""
|
|
104
|
+
self._client = client
|
|
105
|
+
|
|
106
|
+
def __getattr__(self, name: str) -> Any:
|
|
107
|
+
"""Forward everything else to the real client."""
|
|
108
|
+
return getattr(self._client, name)
|
|
109
|
+
|
|
110
|
+
async def call_validated(self, *args: Any, **kwargs: Any) -> Any:
|
|
111
|
+
"""``call_validated`` that returns a failed inner envelope instead of raising."""
|
|
112
|
+
try:
|
|
113
|
+
return await self._client.call_validated(*args, **kwargs)
|
|
114
|
+
except CommandFailedError as exc:
|
|
115
|
+
if isinstance(exc.result, dict):
|
|
116
|
+
return exc.result
|
|
117
|
+
raise
|
|
118
|
+
|
|
119
|
+
async def call(self, *args: Any, **kwargs: Any) -> Any:
|
|
120
|
+
"""``call`` that returns a failed inner envelope instead of raising."""
|
|
121
|
+
try:
|
|
122
|
+
return await self._client.call(*args, **kwargs)
|
|
123
|
+
except CommandFailedError as exc:
|
|
124
|
+
if isinstance(exc.result, dict):
|
|
125
|
+
return exc.result
|
|
126
|
+
raise
|