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.
@@ -0,0 +1,142 @@
1
+ """
2
+ Public async client for code-analysis-server (JSON-RPC via mcp-proxy-adapter).
3
+
4
+ Author: Vasiliy Zdanovskiy
5
+ email: vasilyvz@gmail.com
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+
12
+ from code_analysis_client.client import CodeAnalysisAsyncClient
13
+ from code_analysis_client.symbol_tree import SymbolTreeNode
14
+ from code_analysis_client.commands_proxy import ValidatedCommandsProxy
15
+ from code_analysis_client.config import (
16
+ DEFAULT_CLIENT_ADAPTER_CONFIG,
17
+ adapter_settings_from_server_config,
18
+ adapter_settings_to_jsonrpc_kwargs,
19
+ load_server_config,
20
+ )
21
+ from code_analysis_client.exceptions import (
22
+ ClientValidationError,
23
+ CommandFailedError,
24
+ JobFailedError,
25
+ JobTimeoutError,
26
+ QueueJobError,
27
+ VectorizerUnavailableError,
28
+ )
29
+ from code_analysis_client.file_session import FileSessionClient, SessionNotFoundError
30
+ from code_analysis_client.check_chain import (
31
+ CheckChainClient,
32
+ CheckChainConfigInvalidError,
33
+ CheckChainError,
34
+ CheckChainGetResult,
35
+ CheckChainListResult,
36
+ ChainElement,
37
+ Degraded,
38
+ Diagnostic,
39
+ ExtensionChain,
40
+ FileCheckAndPromoteResult,
41
+ )
42
+ from code_analysis_client.worktree import WorktreeClient, WorktreeConflictError
43
+ from code_analysis_client.project import ProjectClient
44
+ from code_analysis_client.queue_wait import QueuedJob
45
+ from code_analysis_client.server_api import (
46
+ CLIENT_FACADE_COMMANDS,
47
+ CST_REMOVED_COMMANDS,
48
+ FILE_SESSION_COMMANDS,
49
+ FILE_SESSION_FACADE_METHODS,
50
+ LEGACY_REMOVED_COMMANDS,
51
+ REMOVED_COMMANDS,
52
+ TRANSFER_FACADE_METHODS,
53
+ UNIVERSAL_FILE_COMMANDS,
54
+ )
55
+ from code_analysis_client.universal_file import UniversalFileClient
56
+ from code_analysis_client.server_schema import (
57
+ fetch_command_schema_from_server,
58
+ parse_schema_from_help_payload,
59
+ )
60
+ from code_analysis_client.validation import (
61
+ prepare_params_for_schema,
62
+ validate_params_against_schema,
63
+ )
64
+ from code_analysis_client.vectorizer_state import (
65
+ ServiceState,
66
+ VectorizerState,
67
+ VectorizerWarning,
68
+ VectorizationWorkerBlock,
69
+ extract_vectorizer_state_from_response,
70
+ safe_get_vectorizer_state,
71
+ safe_get_warnings,
72
+ safe_get_vectorization_worker,
73
+ )
74
+
75
+ __all__ = [
76
+ "DEFAULT_CLIENT_ADAPTER_CONFIG",
77
+ "CLIENT_FACADE_COMMANDS",
78
+ "CST_REMOVED_COMMANDS",
79
+ "ClientValidationError",
80
+ "CodeAnalysisAsyncClient",
81
+ "SymbolTreeNode",
82
+ "CommandFailedError",
83
+ "FILE_SESSION_COMMANDS",
84
+ "FILE_SESSION_FACADE_METHODS",
85
+ "FileSessionClient",
86
+ "JobFailedError",
87
+ "JobTimeoutError",
88
+ "LEGACY_REMOVED_COMMANDS",
89
+ "QueueJobError",
90
+ "QueuedJob",
91
+ "REMOVED_COMMANDS",
92
+ "SessionNotFoundError",
93
+ "TRANSFER_FACADE_METHODS",
94
+ "UNIVERSAL_FILE_COMMANDS",
95
+ "UniversalFileClient",
96
+ "ValidatedCommandsProxy",
97
+ "VectorizerUnavailableError",
98
+ "ServiceState",
99
+ "VectorizerState",
100
+ "VectorizerWarning",
101
+ "VectorizationWorkerBlock",
102
+ "CheckChainClient",
103
+ "CheckChainConfigInvalidError",
104
+ "CheckChainError",
105
+ "CheckChainGetResult",
106
+ "CheckChainListResult",
107
+ "ChainElement",
108
+ "Degraded",
109
+ "Diagnostic",
110
+ "ExtensionChain",
111
+ "FileCheckAndPromoteResult",
112
+ "ProjectClient",
113
+ "WorktreeClient",
114
+ "WorktreeConflictError",
115
+ "adapter_settings_from_server_config",
116
+ "adapter_settings_to_jsonrpc_kwargs",
117
+ "extract_vectorizer_state_from_response",
118
+ "fetch_command_schema_from_server",
119
+ "load_server_config",
120
+ "parse_schema_from_help_payload",
121
+ "prepare_params_for_schema",
122
+ "safe_get_vectorizer_state",
123
+ "safe_get_warnings",
124
+ "safe_get_vectorization_worker",
125
+ "validate_params_against_schema",
126
+ ]
127
+
128
+
129
+ def _read_package_version() -> str:
130
+ """Read installed client version from version.txt or package metadata."""
131
+ vf = Path(__file__).resolve().parent / "version.txt"
132
+ if vf.is_file():
133
+ return vf.read_text(encoding="utf-8").strip()
134
+ try:
135
+ import importlib.metadata as _imd
136
+
137
+ return _imd.version("code-analysis-client")
138
+ except Exception:
139
+ return "0.0.0"
140
+
141
+
142
+ __version__ = _read_package_version()
@@ -0,0 +1,374 @@
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, FacadeCaller
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
+ "FCP_CHECK_TOOL_INSTALL_FAILED",
45
+ "WORKTREE_COMMIT_FAILED",
46
+ "FCP_PROMOTE_RACE",
47
+ "FCP_CANCELLED_BY_CLEANUP",
48
+ "GIT_NOT_AVAILABLE",
49
+ "GIT_NOT_A_REPO",
50
+ "FILE_LOCKED",
51
+ "LOCK_ACQUIRE_FAILED",
52
+ "LOCK_FILE_PATH_FORBIDDEN",
53
+ }
54
+ )
55
+
56
+
57
+ class CheckChainError(ClientValidationError):
58
+ """A check-chain command answered with a server error.
59
+
60
+ Attributes:
61
+ code: the server error code (e.g. ``FCP_PROMOTE_RACE``).
62
+ details: the server error details/data object.
63
+ """
64
+
65
+ def __init__(
66
+ self,
67
+ message: str,
68
+ *,
69
+ code: str,
70
+ field: Optional[str] = None,
71
+ details: Optional[Dict[str, Any]] = None,
72
+ ) -> None:
73
+ """Store the server error code with the message and details."""
74
+ super().__init__(message, field=field or "command", details=details)
75
+ self.code = code
76
+
77
+
78
+ class CheckChainConfigInvalidError(CheckChainError):
79
+ """CHECK_CHAIN_CONFIG_INVALID with its section, extension and reason."""
80
+
81
+ def __init__(self, message: str, *, details: Optional[Dict[str, Any]] = None) -> None:
82
+ """Expose details.section/extension/reason as attributes."""
83
+ super().__init__(
84
+ message, code="CHECK_CHAIN_CONFIG_INVALID", field="check_chains", details=details
85
+ )
86
+ self.section = self.details.get("section")
87
+ self.extension = self.details.get("extension")
88
+ self.reason = self.details.get("reason")
89
+
90
+
91
+ @dataclass(frozen=True)
92
+ class ChainElement:
93
+ """One check-chain element."""
94
+
95
+ tool: str
96
+ config_source: str
97
+ blocking: bool
98
+ args: List[str] = field(default_factory=list)
99
+
100
+
101
+ @dataclass(frozen=True)
102
+ class ExtensionChain:
103
+ """Resolved chain of one extension."""
104
+
105
+ extension: str
106
+ source: str
107
+ chain: List[ChainElement]
108
+
109
+
110
+ @dataclass(frozen=True)
111
+ class CheckChainListResult:
112
+ """Result of ``check_chain_list``."""
113
+
114
+ project_id: str
115
+ chains: List[ExtensionChain]
116
+
117
+
118
+ @dataclass(frozen=True)
119
+ class CheckChainGetResult:
120
+ """Result of ``check_chain_get``."""
121
+
122
+ project_id: str
123
+ extension: str
124
+ source: str
125
+ chain: List[ChainElement]
126
+
127
+
128
+ @dataclass(frozen=True)
129
+ class Diagnostic:
130
+ """One finding of a chain element."""
131
+
132
+ tool: str
133
+ path: str
134
+ line: Optional[int]
135
+ column: Optional[int]
136
+ code: Optional[str]
137
+ severity: Optional[str]
138
+ message: str
139
+
140
+
141
+ @dataclass(frozen=True)
142
+ class Degraded:
143
+ """Why a call degraded (nothing promoted)."""
144
+
145
+ reason: str
146
+ tool: Optional[str] = None
147
+ skipped_tools: List[str] = field(default_factory=list)
148
+
149
+
150
+ @dataclass(frozen=True)
151
+ class FileCheckAndPromoteResult:
152
+ """Result of ``file_check_and_promote`` (outcome-discriminated).
153
+
154
+ ``outcome`` is ``promoted``, ``diagnostics`` or ``degraded``. ``promoted`` is
155
+ True only for ``promoted``. ``degraded`` may also be set on a promoted outcome
156
+ when only non-blocking tools were skipped (``skipped_tools``).
157
+ """
158
+
159
+ project_id: str
160
+ editor_session_id: str
161
+ path: str
162
+ outcome: str
163
+ promoted: bool
164
+ commit: Optional[str]
165
+ index_pending: bool
166
+ index_job_id: Optional[str]
167
+ index_trigger_error: Optional[str]
168
+ diagnostics: List[Diagnostic]
169
+ degraded: Optional[Degraded]
170
+ raw: Dict[str, Any] = field(default_factory=dict, compare=False, repr=False)
171
+ # Delivered write-back of a promoted documentation copy: {document_id, file_path,
172
+ # status "transferred"}; None for an ordinary file. A failed write-back is not a result:
173
+ # the server answers an error (CheckChainError, e.g. DOCUMENTATION_SERVER_UNAVAILABLE)
174
+ # whose details carry promoted, commit, edit_kept and holder_session_id.
175
+ documentation_transfer: Optional[Dict[str, Any]] = None
176
+ # Seconds spent waiting for the path lock and the project main lock
177
+ # ({path_waited_seconds, main_waited_seconds}); None from an older server.
178
+ lock_wait: Optional[Dict[str, float]] = None
179
+
180
+
181
+ def _require_non_empty(value: Any, *, field_name: str) -> str:
182
+ """Return stripped text or raise ClientValidationError."""
183
+ text = str(value or "").strip()
184
+ if not text:
185
+ raise ClientValidationError(f"{field_name} is required", field=field_name)
186
+ return text
187
+
188
+
189
+ def _error_parts(data: Dict[str, Any]) -> tuple[str, str, Dict[str, Any]]:
190
+ """Return (code, message, details) of a failed response."""
191
+ code = command_error_code(data)
192
+ err = data.get("error")
193
+ err_dict: Dict[str, Any] = err if isinstance(err, dict) else {}
194
+ message = data.get("message") or err_dict.get("message") or code or str(data)
195
+ details = (
196
+ err_dict.get("details")
197
+ or err_dict.get("data")
198
+ or data.get("details")
199
+ or data.get("data")
200
+ or {}
201
+ )
202
+ return code, str(message), details if isinstance(details, dict) else {}
203
+
204
+
205
+ def _success_payload(data: Dict[str, Any], *, command: str) -> Dict[str, Any]:
206
+ """Return the success data dict, or raise the mapped error for a failure."""
207
+ if data.get("success"):
208
+ inner = data.get("data")
209
+ return inner if isinstance(inner, dict) else data
210
+ code, message, details = _error_parts(data)
211
+ if code == "WORKTREE_SYNC_CONFLICT":
212
+ raise WorktreeConflictError(
213
+ message,
214
+ conflicts=details.get("conflicts"),
215
+ preserved_locked_files=details.get("preserved_locked_files"),
216
+ )
217
+ if code == "CHECK_CHAIN_CONFIG_INVALID":
218
+ raise CheckChainConfigInvalidError(message, details=details)
219
+ raise CheckChainError(
220
+ message, code=code or "UNKNOWN", field=details.get("field") or command, details=details
221
+ )
222
+
223
+
224
+ def _element(raw: Any) -> ChainElement:
225
+ """Map one raw chain element."""
226
+ item = raw if isinstance(raw, dict) else {}
227
+ return ChainElement(
228
+ tool=str(item.get("tool", "")),
229
+ config_source=str(item.get("config_source", "auto")),
230
+ blocking=bool(item.get("blocking", False)),
231
+ args=[str(a) for a in (item.get("args") or [])],
232
+ )
233
+
234
+
235
+ def _diagnostic(raw: Any) -> Diagnostic:
236
+ """Map one raw diagnostic."""
237
+ item = raw if isinstance(raw, dict) else {}
238
+ return Diagnostic(
239
+ tool=str(item.get("tool", "")),
240
+ path=str(item.get("path", "")),
241
+ line=item.get("line"),
242
+ column=item.get("column"),
243
+ code=item.get("code"),
244
+ severity=item.get("severity"),
245
+ message=str(item.get("message", "")),
246
+ )
247
+
248
+
249
+ def _degraded(raw: Any) -> Optional[Degraded]:
250
+ """Map the degraded object (None when absent)."""
251
+ if not isinstance(raw, dict):
252
+ return None
253
+ return Degraded(
254
+ reason=str(raw.get("reason", "")),
255
+ tool=raw.get("tool"),
256
+ skipped_tools=[str(t) for t in (raw.get("skipped_tools") or [])],
257
+ )
258
+
259
+
260
+ class CheckChainClient:
261
+ """check_chain_list / check_chain_get / file_check_and_promote."""
262
+
263
+ __slots__ = ("_client",)
264
+
265
+ def __init__(self, client: CodeAnalysisAsyncClient) -> None:
266
+ """Store the underlying async client."""
267
+ self._client = FacadeCaller(client) # type: ignore[assignment]
268
+
269
+ async def list(self, project_id: str) -> CheckChainListResult:
270
+ """Return the resolved chain of every configured extension.
271
+
272
+ Calls ``check_chain_list``. Raises CheckChainConfigInvalidError for a
273
+ malformed registry entry and CheckChainError for other server errors.
274
+ """
275
+ project_id = _require_non_empty(project_id, field_name="project_id")
276
+ data = await self._client.call_validated(
277
+ "check_chain_list", {"project_id": project_id}
278
+ )
279
+ payload = _success_payload(data, command="check_chain_list")
280
+ return CheckChainListResult(
281
+ project_id=str(payload.get("project_id", project_id)),
282
+ chains=[
283
+ ExtensionChain(
284
+ extension=str(entry.get("extension", "")),
285
+ source=str(entry.get("source", "empty")),
286
+ chain=[_element(e) for e in (entry.get("chain") or [])],
287
+ )
288
+ for entry in (payload.get("chains") or [])
289
+ if isinstance(entry, dict)
290
+ ],
291
+ )
292
+
293
+ async def get(self, project_id: str, extension: str) -> CheckChainGetResult:
294
+ """Return the resolved chain of one extension (leading dot required).
295
+
296
+ Calls ``check_chain_get``. An unconfigured extension is source ``empty``
297
+ with an empty chain.
298
+ """
299
+ project_id = _require_non_empty(project_id, field_name="project_id")
300
+ extension = _require_non_empty(extension, field_name="extension")
301
+ if not _EXTENSION_RE.match(extension):
302
+ raise ClientValidationError(
303
+ f"extension must match {EXTENSION_PATTERN}", field="extension"
304
+ )
305
+ data = await self._client.call_validated(
306
+ "check_chain_get", {"project_id": project_id, "extension": extension}
307
+ )
308
+ payload = _success_payload(data, command="check_chain_get")
309
+ return CheckChainGetResult(
310
+ project_id=str(payload.get("project_id", project_id)),
311
+ extension=str(payload.get("extension", extension)),
312
+ source=str(payload.get("source", "empty")),
313
+ chain=[_element(e) for e in (payload.get("chain") or [])],
314
+ )
315
+
316
+ async def file_check_and_promote(
317
+ self,
318
+ project_id: str,
319
+ editor_session_id: str,
320
+ path: str,
321
+ content: str,
322
+ *,
323
+ check_only: bool = False,
324
+ ) -> FileCheckAndPromoteResult:
325
+ """Write, check and promote one file through the editor's worktree.
326
+
327
+ Calls ``file_check_and_promote``. Diagnostics and degraded outcomes are
328
+ successful results (``outcome`` / ``promoted``), never exceptions.
329
+ ``check_only=True`` checks the file in a temporary worktree and promotes
330
+ nothing (``promoted`` False, ``commit`` None).
331
+ Raises WorktreeConflictError on WORKTREE_SYNC_CONFLICT (with
332
+ ``conflicts`` and ``preserved_locked_files``), CheckChainConfigInvalidError
333
+ for a malformed registry entry, and CheckChainError (``code`` from the
334
+ closed enum, e.g. FCP_PROMOTE_RACE) for every other server error. A
335
+ documentation copy whose single write-back failed raises CheckChainError
336
+ too (DOCUMENTATION_SERVER_UNAVAILABLE, NOT_FOUND or INTERNAL_ERROR) although
337
+ the commit is promoted; the edit and the lock stay with the session.
338
+ """
339
+ project_id = _require_non_empty(project_id, field_name="project_id")
340
+ editor_session_id = _require_non_empty(
341
+ editor_session_id, field_name="editor_session_id"
342
+ )
343
+ path = _require_non_empty(path, field_name="path")
344
+ if not isinstance(content, str):
345
+ raise ClientValidationError("content must be a string", field="content")
346
+ if not isinstance(check_only, bool):
347
+ raise ClientValidationError("check_only must be a bool", field="check_only")
348
+ params: Dict[str, Any] = {
349
+ "project_id": project_id,
350
+ "editor_session_id": editor_session_id,
351
+ "path": path,
352
+ "content": content,
353
+ }
354
+ if check_only:
355
+ params["check_only"] = True
356
+ data = await self._client.call_validated("file_check_and_promote", params)
357
+ payload = _success_payload(data, command="file_check_and_promote")
358
+ outcome = str(payload.get("outcome", ""))
359
+ return FileCheckAndPromoteResult(
360
+ project_id=str(payload.get("project_id", project_id)),
361
+ editor_session_id=str(payload.get("editor_session_id", editor_session_id)),
362
+ path=str(payload.get("path", path)),
363
+ outcome=outcome,
364
+ promoted=bool(payload.get("promoted", outcome == "promoted")),
365
+ commit=payload.get("commit"),
366
+ index_pending=bool(payload.get("index_pending", False)),
367
+ index_job_id=payload.get("index_job_id"),
368
+ index_trigger_error=payload.get("index_trigger_error"),
369
+ diagnostics=[_diagnostic(d) for d in (payload.get("diagnostics") or [])],
370
+ degraded=_degraded(payload.get("degraded")),
371
+ raw=payload,
372
+ documentation_transfer=payload.get("documentation_transfer"),
373
+ lock_wait=payload.get("lock_wait"),
374
+ )