code-analysis-client 1.6.178__tar.gz → 1.6.187__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.
- {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/PKG-INFO +2 -2
- {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client/__init__.py +22 -0
- code_analysis_client-1.6.187/code_analysis_client/check_chain.py +350 -0
- {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client/client.py +86 -6
- {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client/exceptions.py +2 -0
- {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client/file_session.py +18 -1
- code_analysis_client-1.6.187/code_analysis_client/version.txt +1 -0
- {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client.egg-info/PKG-INFO +2 -2
- {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client.egg-info/SOURCES.txt +1 -0
- {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client.egg-info/requires.txt +1 -1
- {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/pyproject.toml +1 -1
- code_analysis_client-1.6.178/code_analysis_client/version.txt +0 -1
- {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/README.md +0 -0
- {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client/commands_proxy.py +0 -0
- {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client/config.py +0 -0
- {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client/py.typed +0 -0
- {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client/queue_wait.py +0 -0
- {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client/responses.py +0 -0
- {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client/schema_disk_cache.py +0 -0
- {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client/server_api.py +0 -0
- {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client/server_schema.py +0 -0
- {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client/universal_file.py +0 -0
- {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client/validation.py +0 -0
- {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client/worktree.py +0 -0
- {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client.egg-info/dependency_links.txt +0 -0
- {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client.egg-info/top_level.txt +0 -0
- {code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/setup.cfg +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-analysis-client
|
|
3
|
-
Version: 1.6.
|
|
3
|
+
Version: 1.6.187
|
|
4
4
|
Summary: Async JSON-RPC client for the code-analysis MCP server (mcp-proxy-adapter JsonRpcClient)
|
|
5
5
|
Author-email: Vasiliy Zdanovskiy <vasilyvz@gmail.com>
|
|
6
6
|
Requires-Python: >=3.10
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
|
-
Requires-Dist: mcp-proxy-adapter>=8.10.
|
|
8
|
+
Requires-Dist: mcp-proxy-adapter>=8.10.69
|
|
9
9
|
Provides-Extra: dev
|
|
10
10
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
11
11
|
Requires-Dist: pytest-asyncio>=0.25.0; extra == "dev"
|
{code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client/__init__.py
RENAMED
|
@@ -24,6 +24,18 @@ from code_analysis_client.exceptions import (
|
|
|
24
24
|
QueueJobError,
|
|
25
25
|
)
|
|
26
26
|
from code_analysis_client.file_session import FileSessionClient, SessionNotFoundError
|
|
27
|
+
from code_analysis_client.check_chain import (
|
|
28
|
+
CheckChainClient,
|
|
29
|
+
CheckChainConfigInvalidError,
|
|
30
|
+
CheckChainError,
|
|
31
|
+
CheckChainGetResult,
|
|
32
|
+
CheckChainListResult,
|
|
33
|
+
ChainElement,
|
|
34
|
+
Degraded,
|
|
35
|
+
Diagnostic,
|
|
36
|
+
ExtensionChain,
|
|
37
|
+
FileCheckAndPromoteResult,
|
|
38
|
+
)
|
|
27
39
|
from code_analysis_client.worktree import WorktreeClient, WorktreeConflictError
|
|
28
40
|
from code_analysis_client.queue_wait import (
|
|
29
41
|
QueuedJob,
|
|
@@ -72,6 +84,16 @@ __all__ = [
|
|
|
72
84
|
"UNIVERSAL_FILE_COMMANDS",
|
|
73
85
|
"UniversalFileClient",
|
|
74
86
|
"ValidatedCommandsProxy",
|
|
87
|
+
"CheckChainClient",
|
|
88
|
+
"CheckChainConfigInvalidError",
|
|
89
|
+
"CheckChainError",
|
|
90
|
+
"CheckChainGetResult",
|
|
91
|
+
"CheckChainListResult",
|
|
92
|
+
"ChainElement",
|
|
93
|
+
"Degraded",
|
|
94
|
+
"Diagnostic",
|
|
95
|
+
"ExtensionChain",
|
|
96
|
+
"FileCheckAndPromoteResult",
|
|
75
97
|
"WorktreeClient",
|
|
76
98
|
"WorktreeConflictError",
|
|
77
99
|
"adapter_settings_from_server_config",
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Check-chain commands on top of CodeAnalysisAsyncClient.
|
|
3
|
+
|
|
4
|
+
Typed wrappers for ``check_chain_list``, ``check_chain_get`` and
|
|
5
|
+
``file_check_and_promote`` with full parameter mapping and result/error mapping:
|
|
6
|
+
diagnostics, degraded outcomes, sync conflicts and the closed error enum of
|
|
7
|
+
``file_check_and_promote``.
|
|
8
|
+
|
|
9
|
+
Author: Vasiliy Zdanovskiy
|
|
10
|
+
email: vasilyvz@gmail.com
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
|
18
|
+
|
|
19
|
+
from code_analysis_client.exceptions import ClientValidationError
|
|
20
|
+
from code_analysis_client.responses import command_error_code
|
|
21
|
+
from code_analysis_client.worktree import WorktreeConflictError
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from code_analysis_client.client import CodeAnalysisAsyncClient
|
|
25
|
+
|
|
26
|
+
EXTENSION_PATTERN = r"^\.[A-Za-z0-9]+(\.[A-Za-z0-9]+)*$"
|
|
27
|
+
_EXTENSION_RE = re.compile(EXTENSION_PATTERN)
|
|
28
|
+
|
|
29
|
+
FCP_ERROR_CODES = frozenset(
|
|
30
|
+
{
|
|
31
|
+
"VALIDATION_ERROR",
|
|
32
|
+
"PROJECT_NOT_FOUND",
|
|
33
|
+
"PROJECT_LOCKED",
|
|
34
|
+
"SESSION_NOT_FOUND",
|
|
35
|
+
"WORKTREE_CREATE_FAILED",
|
|
36
|
+
"WORKTREE_CREATE_TIMEOUT",
|
|
37
|
+
"WORKTREE_SYNC_CONFLICT",
|
|
38
|
+
"WORKTREE_MERGE_IN_PROGRESS",
|
|
39
|
+
"WORKTREE_REBASE_IN_PROGRESS",
|
|
40
|
+
"FCP_WRITE_FAILED",
|
|
41
|
+
"CHECK_CHAIN_CONFIG_INVALID",
|
|
42
|
+
"FCP_CHECK_TOOL_CRASHED",
|
|
43
|
+
"FCP_CHECK_TOOL_TIMEOUT",
|
|
44
|
+
"WORKTREE_COMMIT_FAILED",
|
|
45
|
+
"FCP_PROMOTE_RACE",
|
|
46
|
+
"GIT_NOT_AVAILABLE",
|
|
47
|
+
"GIT_NOT_A_REPO",
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class CheckChainError(ClientValidationError):
|
|
53
|
+
"""A check-chain command answered with a server error.
|
|
54
|
+
|
|
55
|
+
Attributes:
|
|
56
|
+
code: the server error code (e.g. ``FCP_PROMOTE_RACE``).
|
|
57
|
+
details: the server error details/data object.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
message: str,
|
|
63
|
+
*,
|
|
64
|
+
code: str,
|
|
65
|
+
field: Optional[str] = None,
|
|
66
|
+
details: Optional[Dict[str, Any]] = None,
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Store the server error code with the message and details."""
|
|
69
|
+
super().__init__(message, field=field or "command", details=details)
|
|
70
|
+
self.code = code
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class CheckChainConfigInvalidError(CheckChainError):
|
|
74
|
+
"""CHECK_CHAIN_CONFIG_INVALID with its section, extension and reason."""
|
|
75
|
+
|
|
76
|
+
def __init__(self, message: str, *, details: Optional[Dict[str, Any]] = None) -> None:
|
|
77
|
+
"""Expose details.section/extension/reason as attributes."""
|
|
78
|
+
super().__init__(
|
|
79
|
+
message, code="CHECK_CHAIN_CONFIG_INVALID", field="check_chains", details=details
|
|
80
|
+
)
|
|
81
|
+
self.section = self.details.get("section")
|
|
82
|
+
self.extension = self.details.get("extension")
|
|
83
|
+
self.reason = self.details.get("reason")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(frozen=True)
|
|
87
|
+
class ChainElement:
|
|
88
|
+
"""One check-chain element."""
|
|
89
|
+
|
|
90
|
+
tool: str
|
|
91
|
+
config_source: str
|
|
92
|
+
blocking: bool
|
|
93
|
+
args: List[str] = field(default_factory=list)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass(frozen=True)
|
|
97
|
+
class ExtensionChain:
|
|
98
|
+
"""Resolved chain of one extension."""
|
|
99
|
+
|
|
100
|
+
extension: str
|
|
101
|
+
source: str
|
|
102
|
+
chain: List[ChainElement]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass(frozen=True)
|
|
106
|
+
class CheckChainListResult:
|
|
107
|
+
"""Result of ``check_chain_list``."""
|
|
108
|
+
|
|
109
|
+
project_id: str
|
|
110
|
+
chains: List[ExtensionChain]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass(frozen=True)
|
|
114
|
+
class CheckChainGetResult:
|
|
115
|
+
"""Result of ``check_chain_get``."""
|
|
116
|
+
|
|
117
|
+
project_id: str
|
|
118
|
+
extension: str
|
|
119
|
+
source: str
|
|
120
|
+
chain: List[ChainElement]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass(frozen=True)
|
|
124
|
+
class Diagnostic:
|
|
125
|
+
"""One finding of a chain element."""
|
|
126
|
+
|
|
127
|
+
tool: str
|
|
128
|
+
path: str
|
|
129
|
+
line: Optional[int]
|
|
130
|
+
column: Optional[int]
|
|
131
|
+
code: Optional[str]
|
|
132
|
+
severity: Optional[str]
|
|
133
|
+
message: str
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@dataclass(frozen=True)
|
|
137
|
+
class Degraded:
|
|
138
|
+
"""Why a call degraded (nothing promoted)."""
|
|
139
|
+
|
|
140
|
+
reason: str
|
|
141
|
+
tool: Optional[str] = None
|
|
142
|
+
skipped_tools: List[str] = field(default_factory=list)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@dataclass(frozen=True)
|
|
146
|
+
class FileCheckAndPromoteResult:
|
|
147
|
+
"""Result of ``file_check_and_promote`` (outcome-discriminated).
|
|
148
|
+
|
|
149
|
+
``outcome`` is ``promoted``, ``diagnostics`` or ``degraded``. ``promoted`` is
|
|
150
|
+
True only for ``promoted``. ``degraded`` may also be set on a promoted outcome
|
|
151
|
+
when only non-blocking tools were skipped (``skipped_tools``).
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
project_id: str
|
|
155
|
+
editor_session_id: str
|
|
156
|
+
path: str
|
|
157
|
+
outcome: str
|
|
158
|
+
promoted: bool
|
|
159
|
+
commit: Optional[str]
|
|
160
|
+
index_pending: bool
|
|
161
|
+
index_job_id: Optional[str]
|
|
162
|
+
index_trigger_error: Optional[str]
|
|
163
|
+
diagnostics: List[Diagnostic]
|
|
164
|
+
degraded: Optional[Degraded]
|
|
165
|
+
raw: Dict[str, Any] = field(default_factory=dict, compare=False, repr=False)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _require_non_empty(value: Any, *, field_name: str) -> str:
|
|
169
|
+
"""Return stripped text or raise ClientValidationError."""
|
|
170
|
+
text = str(value or "").strip()
|
|
171
|
+
if not text:
|
|
172
|
+
raise ClientValidationError(f"{field_name} is required", field=field_name)
|
|
173
|
+
return text
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _error_parts(data: Dict[str, Any]) -> tuple[str, str, Dict[str, Any]]:
|
|
177
|
+
"""Return (code, message, details) of a failed response."""
|
|
178
|
+
code = command_error_code(data)
|
|
179
|
+
err = data.get("error")
|
|
180
|
+
err_dict: Dict[str, Any] = err if isinstance(err, dict) else {}
|
|
181
|
+
message = data.get("message") or err_dict.get("message") or code or str(data)
|
|
182
|
+
details = (
|
|
183
|
+
err_dict.get("details")
|
|
184
|
+
or err_dict.get("data")
|
|
185
|
+
or data.get("details")
|
|
186
|
+
or data.get("data")
|
|
187
|
+
or {}
|
|
188
|
+
)
|
|
189
|
+
return code, str(message), details if isinstance(details, dict) else {}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _success_payload(data: Dict[str, Any], *, command: str) -> Dict[str, Any]:
|
|
193
|
+
"""Return the success data dict, or raise the mapped error for a failure."""
|
|
194
|
+
if data.get("success"):
|
|
195
|
+
inner = data.get("data")
|
|
196
|
+
return inner if isinstance(inner, dict) else data
|
|
197
|
+
code, message, details = _error_parts(data)
|
|
198
|
+
if code == "WORKTREE_SYNC_CONFLICT":
|
|
199
|
+
raise WorktreeConflictError(
|
|
200
|
+
message,
|
|
201
|
+
conflicts=details.get("conflicts"),
|
|
202
|
+
preserved_locked_files=details.get("preserved_locked_files"),
|
|
203
|
+
)
|
|
204
|
+
if code == "CHECK_CHAIN_CONFIG_INVALID":
|
|
205
|
+
raise CheckChainConfigInvalidError(message, details=details)
|
|
206
|
+
raise CheckChainError(
|
|
207
|
+
message, code=code or "UNKNOWN", field=details.get("field") or command, details=details
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _element(raw: Any) -> ChainElement:
|
|
212
|
+
"""Map one raw chain element."""
|
|
213
|
+
item = raw if isinstance(raw, dict) else {}
|
|
214
|
+
return ChainElement(
|
|
215
|
+
tool=str(item.get("tool", "")),
|
|
216
|
+
config_source=str(item.get("config_source", "auto")),
|
|
217
|
+
blocking=bool(item.get("blocking", False)),
|
|
218
|
+
args=[str(a) for a in (item.get("args") or [])],
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _diagnostic(raw: Any) -> Diagnostic:
|
|
223
|
+
"""Map one raw diagnostic."""
|
|
224
|
+
item = raw if isinstance(raw, dict) else {}
|
|
225
|
+
return Diagnostic(
|
|
226
|
+
tool=str(item.get("tool", "")),
|
|
227
|
+
path=str(item.get("path", "")),
|
|
228
|
+
line=item.get("line"),
|
|
229
|
+
column=item.get("column"),
|
|
230
|
+
code=item.get("code"),
|
|
231
|
+
severity=item.get("severity"),
|
|
232
|
+
message=str(item.get("message", "")),
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _degraded(raw: Any) -> Optional[Degraded]:
|
|
237
|
+
"""Map the degraded object (None when absent)."""
|
|
238
|
+
if not isinstance(raw, dict):
|
|
239
|
+
return None
|
|
240
|
+
return Degraded(
|
|
241
|
+
reason=str(raw.get("reason", "")),
|
|
242
|
+
tool=raw.get("tool"),
|
|
243
|
+
skipped_tools=[str(t) for t in (raw.get("skipped_tools") or [])],
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class CheckChainClient:
|
|
248
|
+
"""check_chain_list / check_chain_get / file_check_and_promote."""
|
|
249
|
+
|
|
250
|
+
__slots__ = ("_client",)
|
|
251
|
+
|
|
252
|
+
def __init__(self, client: CodeAnalysisAsyncClient) -> None:
|
|
253
|
+
"""Store the underlying async client."""
|
|
254
|
+
self._client = client
|
|
255
|
+
|
|
256
|
+
async def list(self, project_id: str) -> CheckChainListResult:
|
|
257
|
+
"""Return the resolved chain of every configured extension.
|
|
258
|
+
|
|
259
|
+
Calls ``check_chain_list``. Raises CheckChainConfigInvalidError for a
|
|
260
|
+
malformed registry entry and CheckChainError for other server errors.
|
|
261
|
+
"""
|
|
262
|
+
project_id = _require_non_empty(project_id, field_name="project_id")
|
|
263
|
+
data = await self._client.call_validated(
|
|
264
|
+
"check_chain_list", {"project_id": project_id}
|
|
265
|
+
)
|
|
266
|
+
payload = _success_payload(data, command="check_chain_list")
|
|
267
|
+
return CheckChainListResult(
|
|
268
|
+
project_id=str(payload.get("project_id", project_id)),
|
|
269
|
+
chains=[
|
|
270
|
+
ExtensionChain(
|
|
271
|
+
extension=str(entry.get("extension", "")),
|
|
272
|
+
source=str(entry.get("source", "empty")),
|
|
273
|
+
chain=[_element(e) for e in (entry.get("chain") or [])],
|
|
274
|
+
)
|
|
275
|
+
for entry in (payload.get("chains") or [])
|
|
276
|
+
if isinstance(entry, dict)
|
|
277
|
+
],
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
async def get(self, project_id: str, extension: str) -> CheckChainGetResult:
|
|
281
|
+
"""Return the resolved chain of one extension (leading dot required).
|
|
282
|
+
|
|
283
|
+
Calls ``check_chain_get``. An unconfigured extension is source ``empty``
|
|
284
|
+
with an empty chain.
|
|
285
|
+
"""
|
|
286
|
+
project_id = _require_non_empty(project_id, field_name="project_id")
|
|
287
|
+
extension = _require_non_empty(extension, field_name="extension")
|
|
288
|
+
if not _EXTENSION_RE.match(extension):
|
|
289
|
+
raise ClientValidationError(
|
|
290
|
+
f"extension must match {EXTENSION_PATTERN}", field="extension"
|
|
291
|
+
)
|
|
292
|
+
data = await self._client.call_validated(
|
|
293
|
+
"check_chain_get", {"project_id": project_id, "extension": extension}
|
|
294
|
+
)
|
|
295
|
+
payload = _success_payload(data, command="check_chain_get")
|
|
296
|
+
return CheckChainGetResult(
|
|
297
|
+
project_id=str(payload.get("project_id", project_id)),
|
|
298
|
+
extension=str(payload.get("extension", extension)),
|
|
299
|
+
source=str(payload.get("source", "empty")),
|
|
300
|
+
chain=[_element(e) for e in (payload.get("chain") or [])],
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
async def file_check_and_promote(
|
|
304
|
+
self,
|
|
305
|
+
project_id: str,
|
|
306
|
+
editor_session_id: str,
|
|
307
|
+
path: str,
|
|
308
|
+
content: str,
|
|
309
|
+
) -> FileCheckAndPromoteResult:
|
|
310
|
+
"""Write, check and promote one file through the editor's worktree.
|
|
311
|
+
|
|
312
|
+
Calls ``file_check_and_promote``. Diagnostics and degraded outcomes are
|
|
313
|
+
successful results (``outcome`` / ``promoted``), never exceptions.
|
|
314
|
+
Raises WorktreeConflictError on WORKTREE_SYNC_CONFLICT (with
|
|
315
|
+
``conflicts`` and ``preserved_locked_files``), CheckChainConfigInvalidError
|
|
316
|
+
for a malformed registry entry, and CheckChainError (``code`` from the
|
|
317
|
+
closed enum, e.g. FCP_PROMOTE_RACE) for every other server error.
|
|
318
|
+
"""
|
|
319
|
+
project_id = _require_non_empty(project_id, field_name="project_id")
|
|
320
|
+
editor_session_id = _require_non_empty(
|
|
321
|
+
editor_session_id, field_name="editor_session_id"
|
|
322
|
+
)
|
|
323
|
+
path = _require_non_empty(path, field_name="path")
|
|
324
|
+
if not isinstance(content, str):
|
|
325
|
+
raise ClientValidationError("content must be a string", field="content")
|
|
326
|
+
data = await self._client.call_validated(
|
|
327
|
+
"file_check_and_promote",
|
|
328
|
+
{
|
|
329
|
+
"project_id": project_id,
|
|
330
|
+
"editor_session_id": editor_session_id,
|
|
331
|
+
"path": path,
|
|
332
|
+
"content": content,
|
|
333
|
+
},
|
|
334
|
+
)
|
|
335
|
+
payload = _success_payload(data, command="file_check_and_promote")
|
|
336
|
+
outcome = str(payload.get("outcome", ""))
|
|
337
|
+
return FileCheckAndPromoteResult(
|
|
338
|
+
project_id=str(payload.get("project_id", project_id)),
|
|
339
|
+
editor_session_id=str(payload.get("editor_session_id", editor_session_id)),
|
|
340
|
+
path=str(payload.get("path", path)),
|
|
341
|
+
outcome=outcome,
|
|
342
|
+
promoted=bool(payload.get("promoted", outcome == "promoted")),
|
|
343
|
+
commit=payload.get("commit"),
|
|
344
|
+
index_pending=bool(payload.get("index_pending", False)),
|
|
345
|
+
index_job_id=payload.get("index_job_id"),
|
|
346
|
+
index_trigger_error=payload.get("index_trigger_error"),
|
|
347
|
+
diagnostics=[_diagnostic(d) for d in (payload.get("diagnostics") or [])],
|
|
348
|
+
degraded=_degraded(payload.get("degraded")),
|
|
349
|
+
raw=payload,
|
|
350
|
+
)
|
{code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client/client.py
RENAMED
|
@@ -15,6 +15,7 @@ try:
|
|
|
15
15
|
from mcp_proxy_adapter.client.jsonrpc_client.client import JsonRpcClient
|
|
16
16
|
except Exception: # pragma: no cover - compatibility with older layouts
|
|
17
17
|
from mcp_proxy_adapter.client.jsonrpc_client import JsonRpcClient
|
|
18
|
+
from mcp_proxy_adapter.client.jsonrpc_client.exceptions import ClientRemoteResponseError
|
|
18
19
|
|
|
19
20
|
from code_analysis_client.commands_proxy import ValidatedCommandsProxy
|
|
20
21
|
from code_analysis_client.config import (
|
|
@@ -23,6 +24,7 @@ from code_analysis_client.config import (
|
|
|
23
24
|
load_server_config,
|
|
24
25
|
)
|
|
25
26
|
from code_analysis_client.file_session import FileSessionClient
|
|
27
|
+
from code_analysis_client.check_chain import CheckChainClient
|
|
26
28
|
from code_analysis_client.worktree import WorktreeClient
|
|
27
29
|
from code_analysis_client.queue_wait import (
|
|
28
30
|
QueuedJob,
|
|
@@ -39,12 +41,40 @@ from code_analysis_client.schema_disk_cache import (
|
|
|
39
41
|
store_cached_schema,
|
|
40
42
|
)
|
|
41
43
|
from code_analysis_client.server_schema import fetch_command_schema_from_server
|
|
44
|
+
from code_analysis_client.exceptions import ClientValidationError
|
|
42
45
|
from code_analysis_client.validation import (
|
|
43
46
|
prepare_params_for_schema,
|
|
44
47
|
validate_params_against_schema,
|
|
45
48
|
)
|
|
46
49
|
|
|
47
50
|
|
|
51
|
+
def _remote_invalid_params_as_client_validation(
|
|
52
|
+
error: ClientRemoteResponseError,
|
|
53
|
+
) -> ClientValidationError | None:
|
|
54
|
+
"""Convert the adapter's JSON-RPC invalid-params response for client callers.
|
|
55
|
+
|
|
56
|
+
``JsonRpcClient.execute_command()`` raises ``ClientRemoteResponseError``
|
|
57
|
+
for every top-level JSON-RPC error. Code-analysis validation errors use
|
|
58
|
+
the standard ``-32602`` code, however, and callers of this package already
|
|
59
|
+
handle local schema failures as ``ClientValidationError``. Preserve the
|
|
60
|
+
adapter's complete message and the server's validation payload while
|
|
61
|
+
presenting both local and remote invalid-parameter failures through that
|
|
62
|
+
one client contract.
|
|
63
|
+
"""
|
|
64
|
+
details = error.details
|
|
65
|
+
if details.get("code") != -32602:
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
payload = details.get("data")
|
|
69
|
+
payload = payload if isinstance(payload, dict) else {}
|
|
70
|
+
field = payload.get("field")
|
|
71
|
+
return ClientValidationError(
|
|
72
|
+
str(error),
|
|
73
|
+
field=field if isinstance(field, str) else "command",
|
|
74
|
+
details=payload,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
48
78
|
class CodeAnalysisAsyncClient:
|
|
49
79
|
"""Async client: domain commands via :meth:`call` / :meth:`call_unified`; full adapter API on :attr:`rpc`."""
|
|
50
80
|
|
|
@@ -153,6 +183,11 @@ class CodeAnalysisAsyncClient:
|
|
|
153
183
|
"""Worktree management: ``worktree_*`` commands."""
|
|
154
184
|
return WorktreeClient(self)
|
|
155
185
|
|
|
186
|
+
@property
|
|
187
|
+
def check_chains(self) -> CheckChainClient:
|
|
188
|
+
"""Check-chain registry and promote: ``check_chain_*``, ``file_check_and_promote``."""
|
|
189
|
+
return CheckChainClient(self)
|
|
190
|
+
|
|
156
191
|
@property
|
|
157
192
|
def universal_files(self) -> UniversalFileClient:
|
|
158
193
|
"""Read-only structured preview (``universal_file_preview`` command)."""
|
|
@@ -220,10 +255,16 @@ class CodeAnalysisAsyncClient:
|
|
|
220
255
|
|
|
221
256
|
A non-queued response is returned unchanged regardless of ``auto_poll``.
|
|
222
257
|
"""
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
258
|
+
try:
|
|
259
|
+
resp = await self._rpc.execute_command(
|
|
260
|
+
command,
|
|
261
|
+
params or {},
|
|
262
|
+
)
|
|
263
|
+
except ClientRemoteResponseError as exc:
|
|
264
|
+
validation_error = _remote_invalid_params_as_client_validation(exc)
|
|
265
|
+
if validation_error is None:
|
|
266
|
+
raise
|
|
267
|
+
raise validation_error from exc
|
|
227
268
|
if not is_queued_envelope(resp):
|
|
228
269
|
return resp
|
|
229
270
|
|
|
@@ -240,6 +281,45 @@ class CodeAnalysisAsyncClient:
|
|
|
240
281
|
)
|
|
241
282
|
return await unwrap_job_result(status, rpc=self._rpc)
|
|
242
283
|
|
|
284
|
+
async def _execute_validated(
|
|
285
|
+
self,
|
|
286
|
+
command: str,
|
|
287
|
+
params: Dict[str, Any],
|
|
288
|
+
*,
|
|
289
|
+
use_cmd_endpoint: bool,
|
|
290
|
+
auto_poll: bool,
|
|
291
|
+
timeout: Optional[float],
|
|
292
|
+
poll_interval: float,
|
|
293
|
+
status_hook: Optional[StatusHook],
|
|
294
|
+
) -> Union[Dict[str, Any], QueuedJob]:
|
|
295
|
+
"""Execute after schema validation and reconcile a stale command catalog.
|
|
296
|
+
|
|
297
|
+
A cached schema can outlive a deployment that removes its command. If
|
|
298
|
+
execution reports JSON-RPC ``-32601``, discard the cached catalog view
|
|
299
|
+
and ask live ``help`` once. A command still present keeps the original
|
|
300
|
+
remote error; an absent command surfaces ``help``'s
|
|
301
|
+
:class:`ClientValidationError` with the remote response as its cause.
|
|
302
|
+
"""
|
|
303
|
+
try:
|
|
304
|
+
return await self._execute(
|
|
305
|
+
command,
|
|
306
|
+
params,
|
|
307
|
+
use_cmd_endpoint=use_cmd_endpoint,
|
|
308
|
+
auto_poll=auto_poll,
|
|
309
|
+
timeout=timeout,
|
|
310
|
+
poll_interval=poll_interval,
|
|
311
|
+
status_hook=status_hook,
|
|
312
|
+
)
|
|
313
|
+
except ClientRemoteResponseError as exc:
|
|
314
|
+
if exc.details.get("code") != -32601:
|
|
315
|
+
raise
|
|
316
|
+
self.clear_command_schema_cache()
|
|
317
|
+
try:
|
|
318
|
+
await self.get_command_schema(command, refresh=True)
|
|
319
|
+
except ClientValidationError as schema_error:
|
|
320
|
+
raise schema_error from exc
|
|
321
|
+
raise
|
|
322
|
+
|
|
243
323
|
@overload
|
|
244
324
|
async def call_validated(
|
|
245
325
|
self,
|
|
@@ -304,7 +384,7 @@ class CodeAnalysisAsyncClient:
|
|
|
304
384
|
merged = dict(params or {})
|
|
305
385
|
prepared = prepare_params_for_schema(merged, schema)
|
|
306
386
|
validate_params_against_schema(prepared, schema, command_name=command)
|
|
307
|
-
return await self.
|
|
387
|
+
return await self._execute_validated(
|
|
308
388
|
command,
|
|
309
389
|
prepared,
|
|
310
390
|
use_cmd_endpoint=use_cmd_endpoint,
|
|
@@ -394,7 +474,7 @@ class CodeAnalysisAsyncClient:
|
|
|
394
474
|
merged = dict(params or {})
|
|
395
475
|
prepared = prepare_params_for_schema(merged, schema)
|
|
396
476
|
validate_params_against_schema(prepared, schema, command_name=command)
|
|
397
|
-
return await self.
|
|
477
|
+
return await self._execute_validated(
|
|
398
478
|
command,
|
|
399
479
|
prepared,
|
|
400
480
|
use_cmd_endpoint=use_cmd_endpoint,
|
{code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client/file_session.py
RENAMED
|
@@ -123,11 +123,17 @@ class FileSessionClient:
|
|
|
123
123
|
comment: str,
|
|
124
124
|
*,
|
|
125
125
|
role_ids: Optional[List[str]] = None,
|
|
126
|
+
session_id: Optional[str] = None,
|
|
127
|
+
is_foreign: Optional[bool] = None,
|
|
126
128
|
) -> str:
|
|
127
|
-
"""Create a client session
|
|
129
|
+
"""Create a client session with an optional ID and ownership marker."""
|
|
128
130
|
params: Dict[str, Any] = {"comment": comment}
|
|
129
131
|
if role_ids is not None:
|
|
130
132
|
params["role_ids"] = role_ids
|
|
133
|
+
if session_id is not None:
|
|
134
|
+
params["session_id"] = session_id
|
|
135
|
+
if is_foreign is not None:
|
|
136
|
+
params["is_foreign"] = is_foreign
|
|
131
137
|
payload = _unwrap(await self._client.call_validated("session_create", params))
|
|
132
138
|
sid = str(payload.get("session_id") or "").strip()
|
|
133
139
|
if not sid:
|
|
@@ -177,6 +183,17 @@ class FileSessionClient:
|
|
|
177
183
|
)
|
|
178
184
|
)
|
|
179
185
|
|
|
186
|
+
async def delete_foreign_session(
|
|
187
|
+
self, session_id: str, *, force: bool = False
|
|
188
|
+
) -> Dict[str, Any]:
|
|
189
|
+
"""Explicitly delete a caller-ID session (``session_delete_foreign``)."""
|
|
190
|
+
params: Dict[str, Any] = {"session_id": _require_non_empty(session_id, field="session_id")}
|
|
191
|
+
if force:
|
|
192
|
+
params["force"] = True
|
|
193
|
+
return _unwrap(
|
|
194
|
+
await self._client.call_validated("session_delete_foreign", params)
|
|
195
|
+
)
|
|
196
|
+
|
|
180
197
|
async def view_session(self, session_id: str) -> Dict[str, Any]:
|
|
181
198
|
"""Aggregated session view (``session_view``).
|
|
182
199
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.6.187
|
{code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client.egg-info/PKG-INFO
RENAMED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-analysis-client
|
|
3
|
-
Version: 1.6.
|
|
3
|
+
Version: 1.6.187
|
|
4
4
|
Summary: Async JSON-RPC client for the code-analysis MCP server (mcp-proxy-adapter JsonRpcClient)
|
|
5
5
|
Author-email: Vasiliy Zdanovskiy <vasilyvz@gmail.com>
|
|
6
6
|
Requires-Python: >=3.10
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
|
-
Requires-Dist: mcp-proxy-adapter>=8.10.
|
|
8
|
+
Requires-Dist: mcp-proxy-adapter>=8.10.69
|
|
9
9
|
Provides-Extra: dev
|
|
10
10
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
11
11
|
Requires-Dist: pytest-asyncio>=0.25.0; extra == "dev"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
1.6.178
|
|
File without changes
|
{code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client/commands_proxy.py
RENAMED
|
File without changes
|
{code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client/config.py
RENAMED
|
File without changes
|
|
File without changes
|
{code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client/queue_wait.py
RENAMED
|
File without changes
|
{code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client/responses.py
RENAMED
|
File without changes
|
|
File without changes
|
{code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client/server_api.py
RENAMED
|
File without changes
|
{code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client/server_schema.py
RENAMED
|
File without changes
|
{code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client/universal_file.py
RENAMED
|
File without changes
|
{code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client/validation.py
RENAMED
|
File without changes
|
{code_analysis_client-1.6.178 → code_analysis_client-1.6.187}/code_analysis_client/worktree.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|