code-analysis-client 1.0.6__tar.gz → 1.6.39__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.
Files changed (24) hide show
  1. {code_analysis_client-1.0.6 → code_analysis_client-1.6.39}/PKG-INFO +4 -4
  2. {code_analysis_client-1.0.6 → code_analysis_client-1.6.39}/README.md +2 -2
  3. {code_analysis_client-1.0.6 → code_analysis_client-1.6.39}/code_analysis_client/__init__.py +3 -0
  4. {code_analysis_client-1.0.6 → code_analysis_client-1.6.39}/code_analysis_client/client.py +3 -0
  5. {code_analysis_client-1.0.6 → code_analysis_client-1.6.39}/code_analysis_client/commands_proxy.py +3 -0
  6. {code_analysis_client-1.0.6 → code_analysis_client-1.6.39}/code_analysis_client/config.py +3 -0
  7. {code_analysis_client-1.0.6 → code_analysis_client-1.6.39}/code_analysis_client/exceptions.py +1 -0
  8. {code_analysis_client-1.0.6 → code_analysis_client-1.6.39}/code_analysis_client/file_session.py +146 -53
  9. code_analysis_client-1.6.39/code_analysis_client/server_api.py +285 -0
  10. {code_analysis_client-1.0.6 → code_analysis_client-1.6.39}/code_analysis_client/universal_file.py +17 -4
  11. {code_analysis_client-1.0.6 → code_analysis_client-1.6.39}/code_analysis_client/validation.py +3 -6
  12. code_analysis_client-1.6.39/code_analysis_client/version.txt +1 -0
  13. {code_analysis_client-1.0.6 → code_analysis_client-1.6.39}/code_analysis_client.egg-info/PKG-INFO +4 -4
  14. {code_analysis_client-1.0.6 → code_analysis_client-1.6.39}/code_analysis_client.egg-info/requires.txt +1 -1
  15. {code_analysis_client-1.0.6 → code_analysis_client-1.6.39}/pyproject.toml +1 -1
  16. code_analysis_client-1.0.6/code_analysis_client/server_api.py +0 -154
  17. code_analysis_client-1.0.6/code_analysis_client/version.txt +0 -1
  18. {code_analysis_client-1.0.6 → code_analysis_client-1.6.39}/code_analysis_client/py.typed +0 -0
  19. {code_analysis_client-1.0.6 → code_analysis_client-1.6.39}/code_analysis_client/responses.py +0 -0
  20. {code_analysis_client-1.0.6 → code_analysis_client-1.6.39}/code_analysis_client/server_schema.py +0 -0
  21. {code_analysis_client-1.0.6 → code_analysis_client-1.6.39}/code_analysis_client.egg-info/SOURCES.txt +0 -0
  22. {code_analysis_client-1.0.6 → code_analysis_client-1.6.39}/code_analysis_client.egg-info/dependency_links.txt +0 -0
  23. {code_analysis_client-1.0.6 → code_analysis_client-1.6.39}/code_analysis_client.egg-info/top_level.txt +0 -0
  24. {code_analysis_client-1.0.6 → code_analysis_client-1.6.39}/setup.cfg +0 -0
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-analysis-client
3
- Version: 1.0.6
3
+ Version: 1.6.39
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.13
8
+ Requires-Dist: mcp-proxy-adapter>=8.10.15
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"
@@ -97,8 +97,8 @@ Sync checks (in-process registry):
97
97
  pytest tests/test_client_server_api_sync.py tests/test_code_analysis_client.py -k session
98
98
  ```
99
99
 
100
- Package version is in ``client/code_analysis_client/version.txt`` (synced with the
101
- root ``code-analysis`` project via ``scripts/sync_code_analysis_client_version.py``).
100
+ Package version is in the root ``pyproject.toml``; before a client wheel build run
101
+ ``python scripts/sync_code_analysis_client_version.py`` (also done by ``release_build.sh``).
102
102
 
103
103
  ## Examples (this repository)
104
104
 
@@ -85,8 +85,8 @@ Sync checks (in-process registry):
85
85
  pytest tests/test_client_server_api_sync.py tests/test_code_analysis_client.py -k session
86
86
  ```
87
87
 
88
- Package version is in ``client/code_analysis_client/version.txt`` (synced with the
89
- root ``code-analysis`` project via ``scripts/sync_code_analysis_client_version.py``).
88
+ Package version is in the root ``pyproject.toml``; before a client wheel build run
89
+ ``python scripts/sync_code_analysis_client_version.py`` (also done by ``release_build.sh``).
90
90
 
91
91
  ## Examples (this repository)
92
92
 
@@ -25,6 +25,7 @@ from code_analysis_client.server_api import (
25
25
  FILE_SESSION_FACADE_METHODS,
26
26
  LEGACY_REMOVED_COMMANDS,
27
27
  REMOVED_COMMANDS,
28
+ TRANSFER_FACADE_METHODS,
28
29
  UNIVERSAL_FILE_COMMANDS,
29
30
  )
30
31
  from code_analysis_client.universal_file import UniversalFileClient
@@ -48,6 +49,7 @@ __all__ = [
48
49
  "LEGACY_REMOVED_COMMANDS",
49
50
  "REMOVED_COMMANDS",
50
51
  "SessionNotFoundError",
52
+ "TRANSFER_FACADE_METHODS",
51
53
  "UNIVERSAL_FILE_COMMANDS",
52
54
  "UniversalFileClient",
53
55
  "ValidatedCommandsProxy",
@@ -62,6 +64,7 @@ __all__ = [
62
64
 
63
65
 
64
66
  def _read_package_version() -> str:
67
+ """Read installed client version from version.txt or package metadata."""
65
68
  vf = Path(__file__).resolve().parent / "version.txt"
66
69
  if vf.is_file():
67
70
  return vf.read_text(encoding="utf-8").strip()
@@ -228,10 +228,13 @@ class CodeAnalysisAsyncClient:
228
228
  )
229
229
 
230
230
  async def close(self) -> None:
231
+ """Close the underlying JSON-RPC transport."""
231
232
  await self._rpc.close()
232
233
 
233
234
  async def __aenter__(self) -> CodeAnalysisAsyncClient:
235
+ """Enter async context manager and return this client."""
234
236
  return self
235
237
 
236
238
  async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
239
+ """Close the client when leaving an async context manager block."""
237
240
  await self.close()
@@ -19,6 +19,7 @@ class ValidatedCommandsProxy:
19
19
  __slots__ = ("_client",)
20
20
 
21
21
  def __init__(self, client: CodeAnalysisAsyncClient) -> None:
22
+ """Store the client used for schema lookup and validated command calls."""
22
23
  object.__setattr__(self, "_client", client)
23
24
 
24
25
  def clear_schema_cache(self) -> None:
@@ -47,10 +48,12 @@ class ValidatedCommandsProxy:
47
48
  return cast(Dict[str, Any], await self._client.call_validated(command, merged))
48
49
 
49
50
  def __getattr__(self, name: str) -> Any:
51
+ """Return an async validated-call wrapper for an arbitrary command name."""
50
52
  if name.startswith("_"):
51
53
  raise AttributeError(name)
52
54
 
53
55
  async def _bound(**kw: Any) -> Dict[str, Any]:
56
+ """Execute the dynamically selected command with validated keyword params."""
54
57
  return cast(Dict[str, Any], await self._client.call_validated(name, kw))
55
58
 
56
59
  _bound.__name__ = name
@@ -13,12 +13,14 @@ from typing import Any, Mapping
13
13
 
14
14
 
15
15
  def _expand_path(value: Any) -> str | None:
16
+ """Expand a non-empty filesystem path value to an absolute string."""
16
17
  if value is None or value == "":
17
18
  return None
18
19
  return str(Path(str(value)).expanduser().resolve())
19
20
 
20
21
 
21
22
  def _ssl_paths_from_section(ssl_section: Any) -> dict[str, str]:
23
+ """Extract and normalize TLS path fields from a config SSL section."""
22
24
  if not isinstance(ssl_section, dict):
23
25
  return {}
24
26
  out: dict[str, str] = {}
@@ -38,6 +40,7 @@ def _ssl_paths_from_section(ssl_section: Any) -> dict[str, str]:
38
40
 
39
41
 
40
42
  def _network_from_server_config(config: Mapping[str, Any]) -> dict[str, Any]:
43
+ """Return host, port, and protocol settings from a server config dict."""
41
44
  server = config.get("server", {})
42
45
  if not isinstance(server, dict):
43
46
  server = {}
@@ -15,6 +15,7 @@ class ClientValidationError(ValueError):
15
15
  field: Optional[str] = None,
16
16
  details: Optional[Dict[str, Any]] = None,
17
17
  ) -> None:
18
+ """Initialize validation error with optional field and details payload."""
18
19
  super().__init__(message)
19
20
  self.field = field
20
21
  self.details = details or {}
@@ -4,6 +4,21 @@ High-level file transfer and session workflow on top of CodeAnalysisAsyncClient.
4
4
  Wraps ``session_*`` and ``subordinate_session_*`` MCP commands plus transfer and
5
5
  advisory-lock commands that accept ``session_id``.
6
6
 
7
+ Transfer mapping (client façade → server command):
8
+
9
+ * **Download** — ``download`` → ``project_file_transfer_download_begin`` (``file_id``
10
+ required; optional ``project_id`` scopes the lookup). Chunk streaming uses the
11
+ adapter ``download_file`` helper via ``download_to_path``.
12
+ * **Upload** — two façade methods share one save command
13
+ ``project_file_transfer_upload_save``:
14
+
15
+ - ``upload`` — update mode: ``file_id`` only (overwrite an indexed file).
16
+ - ``upload_new`` — create mode: ``project_id`` + ``file_path`` (path must not
17
+ be indexed yet).
18
+
19
+ Both methods upload bytes through the adapter, then call the same save command
20
+ with the completed ``transfer_id``.
21
+
7
22
  Author: Vasiliy Zdanovskiy
8
23
  email: vasilyvz@gmail.com
9
24
  """
@@ -25,6 +40,7 @@ class SessionNotFoundError(ClientValidationError):
25
40
 
26
41
 
27
42
  def _unwrap(data: Dict[str, Any]) -> Dict[str, Any]:
43
+ """Unwrap command result payloads and map missing sessions to SessionNotFoundError."""
28
44
  return unwrap_command_result(
29
45
  data,
30
46
  session_not_found_type=SessionNotFoundError,
@@ -32,6 +48,7 @@ def _unwrap(data: Dict[str, Any]) -> Dict[str, Any]:
32
48
 
33
49
 
34
50
  def _require_non_empty(value: str, *, field: str) -> str:
51
+ """Return stripped text or raise ClientValidationError for an empty field."""
35
52
  text = str(value or "").strip()
36
53
  if not text:
37
54
  raise ClientValidationError(f"{field} is required", field=field)
@@ -39,13 +56,43 @@ def _require_non_empty(value: str, *, field: str) -> str:
39
56
 
40
57
 
41
58
  def _validate_download_target(*, file_id: Optional[str]) -> None:
42
- """Download requires ``file_id``."""
59
+ """Download requires ``file_id`` (server rejects ``file_path``)."""
43
60
  fid = str(file_id or "").strip()
44
61
  if not fid:
45
62
  raise ClientValidationError("file_id is required", field="file_id")
46
63
 
47
64
 
65
+ def _validate_upload_selector(
66
+ *,
67
+ file_id: Optional[str] = None,
68
+ file_path: Optional[str] = None,
69
+ project_id: Optional[str] = None,
70
+ ) -> None:
71
+ """Mirror server ``_validate_upload_selector_params`` for save payloads."""
72
+ fid = str(file_id or "").strip()
73
+ fp = str(file_path or "").strip()
74
+ pid = str(project_id or "").strip()
75
+ if fid and fp:
76
+ raise ClientValidationError(
77
+ "Specify exactly one of file_id or file_path, not both",
78
+ field="file_id",
79
+ )
80
+ if fid:
81
+ return
82
+ if not pid:
83
+ raise ClientValidationError(
84
+ "project_id is required when file_id is omitted",
85
+ field="project_id",
86
+ )
87
+ if not fp:
88
+ raise ClientValidationError(
89
+ "file_path is required when file_id is omitted",
90
+ field="file_path",
91
+ )
92
+
93
+
48
94
  def _lock_mode_from_flag(lock: bool) -> str:
95
+ """Convert a boolean lock flag into the server lock_mode value."""
49
96
  return "full" if lock else "none"
50
97
 
51
98
 
@@ -67,6 +114,7 @@ class FileSessionClient:
67
114
  __slots__ = ("_client",)
68
115
 
69
116
  def __init__(self, client: CodeAnalysisAsyncClient) -> None:
117
+ """Store the underlying async client used to call session commands."""
70
118
  self._client = client
71
119
 
72
120
  async def create_session(
@@ -89,17 +137,19 @@ class FileSessionClient:
89
137
  )
90
138
  return sid
91
139
 
140
+ async def validate_session(
141
+ self, session_id: str, *, touch: bool = False
142
+ ) -> Dict[str, Any]:
143
+ """Confirm ``session_id`` exists (``session_validate``)."""
144
+ sid = _require_non_empty(session_id, field="session_id")
145
+ params: Dict[str, Any] = {"session_id": sid}
146
+ if touch:
147
+ params["touch"] = True
148
+ return _unwrap(await self._client.call_validated("session_validate", params))
149
+
92
150
  async def assert_session_exists(self, session_id: str) -> None:
93
- """Verify ``session_id`` is registered (touch via ``session_list_file_locks``)."""
94
- sid = str(session_id).strip()
95
- if not sid:
96
- raise ClientValidationError("session_id is required", field="session_id")
97
- _unwrap(
98
- await self._client.call_validated(
99
- "session_list_file_locks",
100
- {"session_id": sid},
101
- )
102
- )
151
+ """Verify ``session_id`` is registered (``session_validate``)."""
152
+ await self.validate_session(session_id, touch=False)
103
153
 
104
154
  async def delete_session(
105
155
  self, session_id: str, *, force: bool = False
@@ -107,12 +157,14 @@ class FileSessionClient:
107
157
  """Delete a client session (``session_delete``).
108
158
 
109
159
  ``force`` defaults to false (omit on the wire when false). Safe delete
110
- requires no ``session_file_locks`` and no ``subordinate_sessions`` rows
111
- where this session is ``parent_session_id``. When ``force`` is true,
112
- linked subordinate client sessions and file locks are removed first.
113
-
114
- Returns ``session_id``, ``deleted``, ``released_lock_count``, and
115
- ``released_subordinate_count``.
160
+ requires no ``session_file_locks``, no ``subordinate_sessions`` rows
161
+ where this session is ``parent_session_id``, and no advisory file-lock
162
+ leases. When ``force`` is true, subordinate link rows, DB file locks, and
163
+ advisory leases for this session are released before the session row is
164
+ deleted.
165
+
166
+ Returns ``session_id``, ``deleted``, ``released_lock_count``,
167
+ ``released_subordinate_count``, and ``released_advisory_lease_count``.
116
168
  """
117
169
  params: Dict[str, Any] = {"session_id": session_id}
118
170
  if force:
@@ -129,8 +181,8 @@ class FileSessionClient:
129
181
 
130
182
  Returns ``locked_files_by_project`` (file_id, file_path,
131
183
  project_presentation per project) and ``subordinate_sessions``
132
- (subordinate_session_id, server_uuid, session_presentation,
133
- server_presentation, link_comment).
184
+ (session_id, server_uuid, session_presentation, server_presentation,
185
+ link_comment).
134
186
  """
135
187
  sid = _require_non_empty(session_id, field="session_id")
136
188
  return _unwrap(
@@ -143,13 +195,13 @@ class FileSessionClient:
143
195
  async def create_subordinate_session(
144
196
  self,
145
197
  parent_session_id: str,
146
- subordinate_session_id: str,
147
198
  comment: str,
148
199
  *,
149
200
  server_uuid: Optional[str] = None,
150
201
  ) -> Dict[str, Any]:
151
- """Link subordinate to parent (``subordinate_session_create``).
202
+ """Register leading session on a subordinate server (``subordinate_session_create``).
152
203
 
204
+ Subordinate servers use ``parent_session_id`` as ``session_id``.
153
205
  ``server_uuid`` defaults to the server's ``registration.instance_uuid``
154
206
  when omitted.
155
207
  """
@@ -157,9 +209,6 @@ class FileSessionClient:
157
209
  "parent_session_id": _require_non_empty(
158
210
  parent_session_id, field="parent_session_id"
159
211
  ),
160
- "subordinate_session_id": _require_non_empty(
161
- subordinate_session_id, field="subordinate_session_id"
162
- ),
163
212
  "comment": comment,
164
213
  }
165
214
  if server_uuid is not None:
@@ -171,10 +220,9 @@ class FileSessionClient:
171
220
  async def get_subordinate_session(
172
221
  self,
173
222
  parent_session_id: str,
174
- subordinate_session_id: str,
175
223
  server_uuid: str,
176
224
  ) -> Dict[str, Any]:
177
- """Fetch one subordinate link (``subordinate_session_get``)."""
225
+ """Fetch one subordinate server link (``subordinate_session_get``)."""
178
226
  return _unwrap(
179
227
  await self._client.call_validated(
180
228
  "subordinate_session_get",
@@ -182,9 +230,6 @@ class FileSessionClient:
182
230
  "parent_session_id": _require_non_empty(
183
231
  parent_session_id, field="parent_session_id"
184
232
  ),
185
- "subordinate_session_id": _require_non_empty(
186
- subordinate_session_id, field="subordinate_session_id"
187
- ),
188
233
  "server_uuid": _require_non_empty(server_uuid, field="server_uuid"),
189
234
  },
190
235
  )
@@ -193,7 +238,6 @@ class FileSessionClient:
193
238
  async def update_subordinate_session(
194
239
  self,
195
240
  parent_session_id: str,
196
- subordinate_session_id: str,
197
241
  server_uuid: str,
198
242
  comment: str,
199
243
  ) -> Dict[str, Any]:
@@ -205,9 +249,6 @@ class FileSessionClient:
205
249
  "parent_session_id": _require_non_empty(
206
250
  parent_session_id, field="parent_session_id"
207
251
  ),
208
- "subordinate_session_id": _require_non_empty(
209
- subordinate_session_id, field="subordinate_session_id"
210
- ),
211
252
  "server_uuid": _require_non_empty(server_uuid, field="server_uuid"),
212
253
  "comment": comment,
213
254
  },
@@ -217,12 +258,11 @@ class FileSessionClient:
217
258
  async def delete_subordinate_session(
218
259
  self,
219
260
  parent_session_id: str,
220
- subordinate_session_id: str,
221
261
  server_uuid: str,
222
262
  ) -> Dict[str, Any]:
223
- """Remove subordinate link only (``subordinate_session_delete``).
263
+ """Remove subordinate server link (``subordinate_session_delete``).
224
264
 
225
- Does not delete ``client_sessions`` rows; use :meth:`delete_session` for that.
265
+ Does not delete the leading ``client_sessions`` row; use :meth:`delete_session`.
226
266
  """
227
267
  return _unwrap(
228
268
  await self._client.call_validated(
@@ -231,9 +271,6 @@ class FileSessionClient:
231
271
  "parent_session_id": _require_non_empty(
232
272
  parent_session_id, field="parent_session_id"
233
273
  ),
234
- "subordinate_session_id": _require_non_empty(
235
- subordinate_session_id, field="subordinate_session_id"
236
- ),
237
274
  "server_uuid": _require_non_empty(server_uuid, field="server_uuid"),
238
275
  },
239
276
  )
@@ -243,18 +280,15 @@ class FileSessionClient:
243
280
  self,
244
281
  *,
245
282
  parent_session_id: Optional[str] = None,
246
- subordinate_session_id: Optional[str] = None,
247
283
  server_uuid: Optional[str] = None,
248
284
  ) -> Dict[str, Any]:
249
- """List subordinate links (``subordinate_session_list``).
285
+ """List subordinate server links (``subordinate_session_list``).
250
286
 
251
287
  Returns ``links`` and ``count``. All filter parameters are optional.
252
288
  """
253
289
  params: Dict[str, Any] = {}
254
290
  if parent_session_id is not None:
255
291
  params["parent_session_id"] = parent_session_id
256
- if subordinate_session_id is not None:
257
- params["subordinate_session_id"] = subordinate_session_id
258
292
  if server_uuid is not None:
259
293
  params["server_uuid"] = server_uuid
260
294
  return _unwrap(
@@ -391,9 +425,10 @@ class FileSessionClient:
391
425
  compression: str = "identity",
392
426
  lock: bool = True,
393
427
  chunk_size: Optional[int] = None,
394
- include_backup_history: bool = False,
428
+ include_backup_history: bool = True,
429
+ project_id: Optional[str] = None,
395
430
  ) -> Dict[str, Any]:
396
- """Begin chunked download; returns payload including ``file_id`` and ``transfer_id``."""
431
+ """Internal: call ``project_file_transfer_download_begin``."""
397
432
  fid = _require_non_empty(file_id, field="file_id")
398
433
  _validate_download_target(file_id=fid)
399
434
  await self.assert_session_exists(session_id)
@@ -406,6 +441,8 @@ class FileSessionClient:
406
441
  }
407
442
  if chunk_size is not None:
408
443
  params["chunk_size"] = chunk_size
444
+ if project_id is not None:
445
+ params["project_id"] = _require_non_empty(project_id, field="project_id")
409
446
  payload = _unwrap(
410
447
  await self._client.call_validated(
411
448
  "project_file_transfer_download_begin",
@@ -423,9 +460,14 @@ class FileSessionClient:
423
460
  *,
424
461
  compression: str = "identity",
425
462
  lock: bool = True,
426
- include_backup_history: bool = False,
463
+ include_backup_history: bool = True,
464
+ project_id: Optional[str] = None,
427
465
  ) -> Tuple[Dict[str, Any], Any]:
428
- """Download an indexed file by ``file_id``; returns (begin_payload, receipt)."""
466
+ """Download an indexed file by ``file_id`` (``project_file_transfer_download_begin``).
467
+
468
+ Returns ``(begin_payload, adapter_receipt)``. ``file_path`` is not accepted —
469
+ resolve ``file_id`` from ``list_project_files`` or ``session_view`` first.
470
+ """
429
471
  fid = _require_non_empty(file_id, field="file_id")
430
472
  begin = await self._begin_download(
431
473
  session_id,
@@ -433,6 +475,7 @@ class FileSessionClient:
433
475
  compression=compression,
434
476
  lock=lock,
435
477
  include_backup_history=include_backup_history,
478
+ project_id=project_id,
436
479
  )
437
480
  transfer_id = str(begin.get("transfer_id") or "").strip()
438
481
  if not transfer_id:
@@ -486,8 +529,19 @@ class FileSessionClient:
486
529
  unlock_after_write: bool = True,
487
530
  dry_run: bool = False,
488
531
  backup: bool = True,
532
+ diff: bool = False,
533
+ diff_context_lines: Optional[int] = None,
534
+ commit_message: Optional[str] = None,
535
+ validate_syntax_only: bool = False,
536
+ tree_id: Optional[str] = None,
537
+ lock_mode: Optional[str] = None,
489
538
  ) -> Dict[str, Any]:
490
539
  """Persist a completed adapter upload (``project_file_transfer_upload_save``)."""
540
+ _validate_upload_selector(
541
+ file_id=file_id,
542
+ file_path=file_path,
543
+ project_id=project_id,
544
+ )
491
545
  await self.assert_session_exists(session_id)
492
546
  params: Dict[str, Any] = {
493
547
  "session_id": session_id,
@@ -502,6 +556,18 @@ class FileSessionClient:
502
556
  params["file_id"] = file_id
503
557
  if file_path is not None:
504
558
  params["file_path"] = file_path
559
+ if diff:
560
+ params["diff"] = True
561
+ if diff_context_lines is not None:
562
+ params["diff_context_lines"] = diff_context_lines
563
+ if commit_message is not None:
564
+ params["commit_message"] = commit_message
565
+ if validate_syntax_only:
566
+ params["validate_syntax_only"] = True
567
+ if tree_id is not None:
568
+ params["tree_id"] = tree_id
569
+ if lock_mode is not None:
570
+ params["lock_mode"] = lock_mode
505
571
  return _unwrap(
506
572
  await self._client.call_validated(
507
573
  "project_file_transfer_upload_save",
@@ -521,11 +587,18 @@ class FileSessionClient:
521
587
  unlock: bool = True,
522
588
  dry_run: bool = False,
523
589
  backup: bool = True,
590
+ diff: bool = False,
591
+ diff_context_lines: Optional[int] = None,
592
+ commit_message: Optional[str] = None,
593
+ validate_syntax_only: bool = False,
594
+ tree_id: Optional[str] = None,
595
+ lock_mode: Optional[str] = None,
524
596
  ) -> str:
525
- """Create a new project file from uploaded bytes; returns the new ``file_id``.
597
+ """Create a new project file (``project_file_transfer_upload_save`` create mode).
526
598
 
527
- ``project_id`` and project-relative ``file_path`` are required. The path must
528
- not already be indexed in the database (new file). Server errors otherwise.
599
+ Pass ``project_id`` and project-relative ``file_path`` only do not pass
600
+ ``file_id``. The path must not already be indexed (``FILE_ALREADY_INDEXED``
601
+ otherwise). Returns the new ``file_id`` (may be empty on ``dry_run``).
529
602
  """
530
603
  pid = _require_non_empty(project_id, field="project_id")
531
604
  fp = _require_non_empty(file_path, field="file_path")
@@ -549,6 +622,12 @@ class FileSessionClient:
549
622
  unlock_after_write=unlock,
550
623
  backup=backup,
551
624
  dry_run=dry_run,
625
+ diff=diff,
626
+ diff_context_lines=diff_context_lines,
627
+ commit_message=commit_message,
628
+ validate_syntax_only=validate_syntax_only,
629
+ tree_id=tree_id,
630
+ lock_mode=lock_mode,
552
631
  )
553
632
  if dry_run:
554
633
  fid = str(saved.get("file_id") or "").strip()
@@ -561,16 +640,23 @@ class FileSessionClient:
561
640
  payload: bytes,
562
641
  file_id: str,
563
642
  *,
643
+ project_id: Optional[str] = None,
564
644
  filename: Optional[str] = None,
565
645
  compression: str = "identity",
566
646
  unlock: bool = True,
567
647
  backup: bool = True,
568
648
  dry_run: bool = False,
649
+ diff: bool = False,
650
+ diff_context_lines: Optional[int] = None,
651
+ commit_message: Optional[str] = None,
652
+ validate_syntax_only: bool = False,
653
+ tree_id: Optional[str] = None,
654
+ lock_mode: Optional[str] = None,
569
655
  ) -> Dict[str, Any]:
570
- """Overwrite an existing indexed file from uploaded bytes.
656
+ """Overwrite an existing indexed file (``project_file_transfer_upload_save`` update mode).
571
657
 
572
- Only ``file_id`` is required. Returns the server save payload on success
573
- or raises :class:`ClientValidationError` on failure.
658
+ Pass ``file_id`` only. Optional ``project_id`` must match the row if provided.
659
+ Do not pass ``file_path``. Returns the server save payload.
574
660
  """
575
661
  fid = _require_non_empty(file_id, field="file_id")
576
662
  name = filename or "payload.bin"
@@ -589,9 +675,16 @@ class FileSessionClient:
589
675
  session_id,
590
676
  str(receipt.transfer_id),
591
677
  file_id=fid,
678
+ project_id=project_id,
592
679
  unlock_after_write=unlock,
593
680
  backup=backup,
594
681
  dry_run=dry_run,
682
+ diff=diff,
683
+ diff_context_lines=diff_context_lines,
684
+ commit_message=commit_message,
685
+ validate_syntax_only=validate_syntax_only,
686
+ tree_id=tree_id,
687
+ lock_mode=lock_mode,
595
688
  )
596
689
  if not dry_run:
597
690
  _extract_file_id(saved)
@@ -0,0 +1,285 @@
1
+ """
2
+ Canonical command names for the high-level client API vs the live server registry.
3
+
4
+ **code-analysis-server** owns project trees, the index database, sessions, locks,
5
+ and editor ingress (chunked transfer). It does **not** expose in-server content
6
+ editing (``universal_file_open/edit/write/close``, CST/JSON tree modify/save,
7
+ ``format_code``, legacy line writers).
8
+
9
+ **File content on CA** — read-only surface only:
10
+
11
+ - :data:`FILE_CONTENT_READ_COMMANDS` — preview and project search (``search`` /
12
+ ``search_get_page``); on-disk grep is internal to ``search`` when enabled.
13
+ - :data:`FS_COMMANDS` — filesystem operations (copy/move/remove/list/grep); not
14
+ the structured edit workflow.
15
+
16
+ Editor clients use :data:`CLIENT_FACADE_COMMANDS` (sessions, transfer, locks,
17
+ ``universal_file_preview``).
18
+
19
+ Author: Vasiliy Zdanovskiy
20
+ email: vasilyvz@gmail.com
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from typing import Callable, Dict, FrozenSet, Tuple
26
+
27
+ # Removed from the public server surface — do not use in client facades or docs.
28
+ LEGACY_REMOVED_COMMANDS: FrozenSet[str] = frozenset(
29
+ {
30
+ "universal_file_read",
31
+ "universal_file_save",
32
+ "universal_file_replace",
33
+ "universal_file_delete",
34
+ "read_project_text_file",
35
+ "write_project_text_lines",
36
+ "create_text_file",
37
+ "replace_file_lines",
38
+ "universal_file_search",
39
+ }
40
+ )
41
+
42
+ # CST / JSON tree MCP workflow — modules may exist in the repo but are not registered.
43
+ CST_REMOVED_COMMANDS: FrozenSet[str] = frozenset(
44
+ {
45
+ "cst_load_file",
46
+ "cst_save_tree",
47
+ "cst_modify_tree",
48
+ "cst_create_file",
49
+ "cst_find_node",
50
+ "cst_get_node_info",
51
+ "cst_get_node_by_range",
52
+ "cst_get_node_at_line",
53
+ "cst_reload_tree",
54
+ "cst_convert_and_save",
55
+ "cst_apply_buffer",
56
+ "cst_list_trees",
57
+ "cst_unload_tree",
58
+ "list_cst_blocks",
59
+ "query_cst",
60
+ "json_load_file",
61
+ "json_find_node",
62
+ "json_get_node_info",
63
+ "json_modify_tree",
64
+ "json_save_tree",
65
+ "json_reload_tree",
66
+ }
67
+ )
68
+
69
+ # Content editing on ai-editor-server only (not on code-analysis-server).
70
+ EDITING_REMOVED_COMMANDS: FrozenSet[str] = frozenset(
71
+ {
72
+ "universal_file_open",
73
+ "universal_file_edit",
74
+ "universal_file_write",
75
+ "universal_file_close",
76
+ "universal_file_move_nodes",
77
+ "session_git_log",
78
+ "session_git_diff",
79
+ "session_git_show",
80
+ "session_git_status",
81
+ "session_git_revert",
82
+ "session_undo",
83
+ "session_redo",
84
+ "session_write",
85
+ "delete_file",
86
+ "delete_files_by_mask",
87
+ "restore_deleted_files",
88
+ "unmark_deleted_file",
89
+ "cleanup_deleted_files",
90
+ "collapse_versions",
91
+ "restore_backup_file",
92
+ "delete_backup",
93
+ "clear_all_backups",
94
+ "split_class",
95
+ "extract_superclass",
96
+ "split_file_to_package",
97
+ "format_code",
98
+ }
99
+ )
100
+
101
+ REMOVED_COMMANDS: FrozenSet[str] = (
102
+ LEGACY_REMOVED_COMMANDS | CST_REMOVED_COMMANDS | EDITING_REMOVED_COMMANDS
103
+ )
104
+
105
+ # Read-only file **content** on CA (no mutate/edit workflow). Includes on-disk search
106
+ # (``fs_grep``, paginated ``search_*``) for paths not yet in the index.
107
+ FILE_CONTENT_READ_COMMANDS: FrozenSet[str] = frozenset(
108
+ {
109
+ "universal_file_preview",
110
+ "get_file_lines",
111
+ "search",
112
+ "search_get_page",
113
+ "search_get_status",
114
+ "search_cancel",
115
+ "search_close",
116
+ }
117
+ )
118
+
119
+ # Filesystem helpers — separate from structured content editing; may touch paths/bytes
120
+ # via copy/move/remove but are not universal_file_edit / CST / format_code.
121
+ FS_COMMANDS: FrozenSet[str] = frozenset(
122
+ {
123
+ "fs_copy",
124
+ "fs_move",
125
+ "fs_remove",
126
+ }
127
+ )
128
+
129
+ # Client sessions + DB file locks — persisted on this server for editor/terminal clients.
130
+ FILE_SESSION_COMMANDS: FrozenSet[str] = frozenset(
131
+ {
132
+ "session_create",
133
+ "session_validate",
134
+ "session_delete",
135
+ "session_list",
136
+ "session_view",
137
+ "session_open_file",
138
+ "session_close_file",
139
+ "session_list_file_locks",
140
+ "subordinate_session_create",
141
+ "subordinate_session_get",
142
+ "subordinate_session_update",
143
+ "subordinate_session_delete",
144
+ "subordinate_session_list",
145
+ }
146
+ )
147
+
148
+ TRANSFER_AND_LOCK_COMMANDS: FrozenSet[str] = frozenset(
149
+ {
150
+ "project_file_transfer_download_begin",
151
+ "project_file_transfer_upload_save",
152
+ "project_file_advisory_lock_batch",
153
+ }
154
+ )
155
+
156
+ UNIVERSAL_FILE_COMMANDS: FrozenSet[str] = frozenset(
157
+ {
158
+ "universal_file_preview",
159
+ }
160
+ )
161
+
162
+ CLIENT_FACADE_COMMANDS: FrozenSet[str] = (
163
+ FILE_SESSION_COMMANDS | TRANSFER_AND_LOCK_COMMANDS | UNIVERSAL_FILE_COMMANDS
164
+ )
165
+
166
+ # ``FileSessionClient`` method names for each ``FILE_SESSION_COMMANDS`` entry.
167
+ # ``session_list_file_locks`` is also used by ``assert_session_exists``.
168
+ FILE_SESSION_FACADE_METHODS: Dict[str, str] = {
169
+ "session_create": "create_session",
170
+ "session_validate": "validate_session",
171
+ "session_delete": "delete_session",
172
+ "session_list": "list_sessions",
173
+ "session_view": "view_session",
174
+ "session_open_file": "lock_file",
175
+ "session_close_file": "unlock_file",
176
+ "session_list_file_locks": "list_file_locks",
177
+ "subordinate_session_create": "create_subordinate_session",
178
+ "subordinate_session_get": "get_subordinate_session",
179
+ "subordinate_session_update": "update_subordinate_session",
180
+ "subordinate_session_delete": "delete_subordinate_session",
181
+ "subordinate_session_list": "list_subordinate_sessions",
182
+ }
183
+
184
+ # Transfer / advisory-lock commands may map to more than one façade method.
185
+ TRANSFER_FACADE_METHODS: Dict[str, Tuple[str, ...]] = {
186
+ "project_file_transfer_download_begin": ("download",),
187
+ "project_file_transfer_upload_save": ("upload", "upload_new"),
188
+ "project_file_advisory_lock_batch": (
189
+ "lock_files_advisory",
190
+ "unlock_files_advisory",
191
+ ),
192
+ }
193
+
194
+
195
+ def assert_file_session_facade_complete() -> None:
196
+ """Raise ``AssertionError`` if ``FILE_SESSION_FACADE_METHODS`` is incomplete."""
197
+ missing_cmds = FILE_SESSION_COMMANDS - set(FILE_SESSION_FACADE_METHODS)
198
+ extra_cmds = set(FILE_SESSION_FACADE_METHODS) - FILE_SESSION_COMMANDS
199
+ if missing_cmds or extra_cmds:
200
+ raise AssertionError(
201
+ "FILE_SESSION_FACADE_METHODS out of sync with FILE_SESSION_COMMANDS: "
202
+ f"missing={sorted(missing_cmds)!r} extra={sorted(extra_cmds)!r}"
203
+ )
204
+ from code_analysis_client.file_session import FileSessionClient
205
+
206
+ for command, method_name in FILE_SESSION_FACADE_METHODS.items():
207
+ if not hasattr(FileSessionClient, method_name):
208
+ raise AssertionError(
209
+ f"FileSessionClient missing method {method_name!r} for command {command!r}"
210
+ )
211
+
212
+
213
+ def assert_transfer_facade_complete() -> None:
214
+ """Raise ``AssertionError`` if ``TRANSFER_FACADE_METHODS`` is incomplete."""
215
+ missing_cmds = TRANSFER_AND_LOCK_COMMANDS - set(TRANSFER_FACADE_METHODS)
216
+ extra_cmds = set(TRANSFER_FACADE_METHODS) - TRANSFER_AND_LOCK_COMMANDS
217
+ if missing_cmds or extra_cmds:
218
+ raise AssertionError(
219
+ "TRANSFER_FACADE_METHODS out of sync with TRANSFER_AND_LOCK_COMMANDS: "
220
+ f"missing={sorted(missing_cmds)!r} extra={sorted(extra_cmds)!r}"
221
+ )
222
+ from code_analysis_client.file_session import FileSessionClient
223
+
224
+ for command, method_names in TRANSFER_FACADE_METHODS.items():
225
+ for method_name in method_names:
226
+ if not hasattr(FileSessionClient, method_name):
227
+ raise AssertionError(
228
+ f"FileSessionClient missing method {method_name!r} "
229
+ f"for command {command!r}"
230
+ )
231
+
232
+
233
+ def _command_registered(get_command: Callable[[str], object], name: str) -> bool:
234
+ """Return True when ``get_command`` can resolve a command name."""
235
+ try:
236
+ get_command(name)
237
+ return True
238
+ except KeyError:
239
+ return False
240
+
241
+
242
+ def assert_facade_commands_registered(get_command: Callable[[str], object]) -> None:
243
+ """Raise ``KeyError`` if any facade command is missing from the server registry."""
244
+ missing = [
245
+ name
246
+ for name in sorted(CLIENT_FACADE_COMMANDS)
247
+ if not _command_registered(get_command, name)
248
+ ]
249
+ if missing:
250
+ raise KeyError(f"Client facade commands not registered on server: {missing}")
251
+
252
+
253
+ def assert_removed_commands_absent(get_command: Callable[[str], object]) -> None:
254
+ """Raise ``AssertionError`` if a removed command is still registered."""
255
+ present = [
256
+ name
257
+ for name in sorted(REMOVED_COMMANDS)
258
+ if _command_registered(get_command, name)
259
+ ]
260
+ if present:
261
+ raise AssertionError(f"Removed commands still registered: {present}")
262
+
263
+
264
+ def assert_file_content_read_commands_registered(
265
+ get_command: Callable[[str], object],
266
+ ) -> None:
267
+ """Raise ``KeyError`` if a read-only content command is missing from the registry."""
268
+ missing = [
269
+ name
270
+ for name in sorted(FILE_CONTENT_READ_COMMANDS)
271
+ if not _command_registered(get_command, name)
272
+ ]
273
+ if missing:
274
+ raise KeyError(f"File content read commands not registered: {missing}")
275
+
276
+
277
+ def assert_fs_commands_registered(get_command: Callable[[str], object]) -> None:
278
+ """Raise ``KeyError`` if a filesystem helper command is missing from the registry."""
279
+ missing = [
280
+ name
281
+ for name in sorted(FS_COMMANDS)
282
+ if not _command_registered(get_command, name)
283
+ ]
284
+ if missing:
285
+ raise KeyError(f"Filesystem commands not registered: {missing}")
@@ -20,11 +20,16 @@ if TYPE_CHECKING:
20
20
 
21
21
 
22
22
  class UniversalFileClient:
23
- """Edit-session workflow for project files (open → edit → write → close)."""
23
+ """Edit-session workflow for project files (open → edit → write → close).
24
+
25
+ Uses ``session_id`` from ``universal_file_open`` only. Not related to
26
+ client ``session_*`` commands (``session_create``, ``session_open_file``, …).
27
+ """
24
28
 
25
29
  __slots__ = ("_client",)
26
30
 
27
31
  def __init__(self, client: CodeAnalysisAsyncClient) -> None:
32
+ """Store the async client used for universal file workflow commands."""
28
33
  self._client = client
29
34
 
30
35
  async def open(
@@ -35,7 +40,10 @@ class UniversalFileClient:
35
40
  create: bool = False,
36
41
  initial_content: Optional[str] = None,
37
42
  ) -> Dict[str, Any]:
38
- """Start an edit session (``universal_file_open``)."""
43
+ """Start an edit session (``universal_file_open``).
44
+
45
+ Identifies the file by ``project_id`` + ``file_path`` (not ``file_id``).
46
+ """
39
47
  params: Dict[str, Any] = {
40
48
  "project_id": project_id,
41
49
  "file_path": file_path,
@@ -72,7 +80,12 @@ class UniversalFileClient:
72
80
  *,
73
81
  write_mode: str = "commit",
74
82
  ) -> Dict[str, Any]:
75
- """Persist or preview draft (``universal_file_write``)."""
83
+ """Persist or preview draft (``universal_file_write``).
84
+
85
+ Default ``write_mode`` is ``commit`` in this client wrapper. The server
86
+ default when the parameter is omitted is ``preview``. Pass
87
+ ``write_mode='preview'`` first to inspect the diff, then ``commit``.
88
+ """
76
89
  return unwrap_command_result(
77
90
  await self._client.call_validated(
78
91
  "universal_file_write",
@@ -89,7 +102,7 @@ class UniversalFileClient:
89
102
  project_id: str,
90
103
  session_id: str,
91
104
  ) -> Dict[str, Any]:
92
- """End edit session (``universal_file_close``)."""
105
+ """End edit session (``universal_file_open`` session_id, not client ``session_*``)."""
93
106
  return unwrap_command_result(
94
107
  await self._client.call_validated(
95
108
  "universal_file_close",
@@ -13,12 +13,9 @@ from code_analysis_client.exceptions import ClientValidationError
13
13
 
14
14
 
15
15
  def prepare_params_for_schema(
16
- params: Dict[str, Any], schema: Dict[str, Any]
16
+ params: Dict[str, Any], schema: Dict[str, Any] # noqa: ARG001
17
17
  ) -> Dict[str, Any]:
18
- """Drop unknown keys when ``additionalProperties`` is false (same as server base)."""
19
- props = schema.get("properties") or {}
20
- if not schema.get("additionalProperties", True):
21
- return {k: v for k, v in params.items() if k in props}
18
+ """Return a shallow copy of params for validation and dispatch."""
22
19
  return dict(params)
23
20
 
24
21
 
@@ -34,7 +31,7 @@ def validate_params_against_schema(
34
31
  field="params",
35
32
  )
36
33
  props = schema.get("properties") or {}
37
- additional_ok = schema.get("additionalProperties", True)
34
+ additional_ok = schema.get("additionalProperties", False)
38
35
  required_set = set(schema.get("required") or [])
39
36
  for key, value in params.items():
40
37
  if key not in props:
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-analysis-client
3
- Version: 1.0.6
3
+ Version: 1.6.39
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.13
8
+ Requires-Dist: mcp-proxy-adapter>=8.10.15
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"
@@ -97,8 +97,8 @@ Sync checks (in-process registry):
97
97
  pytest tests/test_client_server_api_sync.py tests/test_code_analysis_client.py -k session
98
98
  ```
99
99
 
100
- Package version is in ``client/code_analysis_client/version.txt`` (synced with the
101
- root ``code-analysis`` project via ``scripts/sync_code_analysis_client_version.py``).
100
+ Package version is in the root ``pyproject.toml``; before a client wheel build run
101
+ ``python scripts/sync_code_analysis_client_version.py`` (also done by ``release_build.sh``).
102
102
 
103
103
  ## Examples (this repository)
104
104
 
@@ -1,4 +1,4 @@
1
- mcp-proxy-adapter>=8.10.13
1
+ mcp-proxy-adapter>=8.10.15
2
2
 
3
3
  [dev]
4
4
  pytest>=8.0
@@ -12,7 +12,7 @@ authors = [
12
12
  { name = "Vasiliy Zdanovskiy", email = "vasilyvz@gmail.com" },
13
13
  ]
14
14
  dependencies = [
15
- "mcp-proxy-adapter>=8.10.13",
15
+ "mcp-proxy-adapter>=8.10.15",
16
16
  ]
17
17
 
18
18
  [project.optional-dependencies]
@@ -1,154 +0,0 @@
1
- """
2
- Canonical command names for the high-level client API vs the live server registry.
3
-
4
- The generic client (:class:`CodeAnalysisAsyncClient.call`) can invoke any command
5
- returned by server ``help``. Facade modules (:mod:`file_session`, :mod:`universal_file`)
6
- must only reference commands listed here — and every name in
7
- :data:`CLIENT_FACADE_COMMANDS` must be registered on the server.
8
-
9
- Author: Vasiliy Zdanovskiy
10
- email: vasilyvz@gmail.com
11
- """
12
-
13
- from __future__ import annotations
14
-
15
- from typing import Callable, Dict, FrozenSet
16
-
17
- # Removed from the public server surface — do not use in client facades or docs.
18
- LEGACY_REMOVED_COMMANDS: FrozenSet[str] = frozenset(
19
- {
20
- "universal_file_read",
21
- "universal_file_save",
22
- "universal_file_replace",
23
- "universal_file_delete",
24
- "read_project_text_file",
25
- "write_project_text_lines",
26
- "create_text_file",
27
- "replace_file_lines",
28
- }
29
- )
30
-
31
- # CST tree MCP workflow — modules may exist in the repo but commands are not registered.
32
- CST_REMOVED_COMMANDS: FrozenSet[str] = frozenset(
33
- {
34
- "cst_load_file",
35
- "cst_save_tree",
36
- "cst_modify_tree",
37
- "cst_create_file",
38
- "cst_find_node",
39
- "cst_get_node_info",
40
- "cst_get_node_by_range",
41
- "cst_get_node_at_line",
42
- "cst_reload_tree",
43
- "cst_convert_and_save",
44
- "list_cst_blocks",
45
- "query_cst",
46
- "get_file_lines",
47
- }
48
- )
49
-
50
- REMOVED_COMMANDS: FrozenSet[str] = LEGACY_REMOVED_COMMANDS | CST_REMOVED_COMMANDS
51
-
52
- FILE_SESSION_COMMANDS: FrozenSet[str] = frozenset(
53
- {
54
- "session_create",
55
- "session_delete",
56
- "session_list",
57
- "session_view",
58
- "session_open_file",
59
- "session_close_file",
60
- "session_list_file_locks",
61
- "subordinate_session_create",
62
- "subordinate_session_get",
63
- "subordinate_session_update",
64
- "subordinate_session_delete",
65
- "subordinate_session_list",
66
- }
67
- )
68
-
69
- TRANSFER_AND_LOCK_COMMANDS: FrozenSet[str] = frozenset(
70
- {
71
- "project_file_transfer_download_begin",
72
- "project_file_transfer_upload_save",
73
- "project_file_advisory_lock_batch",
74
- }
75
- )
76
-
77
- UNIVERSAL_FILE_COMMANDS: FrozenSet[str] = frozenset(
78
- {
79
- "universal_file_open",
80
- "universal_file_edit",
81
- "universal_file_write",
82
- "universal_file_close",
83
- "universal_file_preview",
84
- }
85
- )
86
-
87
- CLIENT_FACADE_COMMANDS: FrozenSet[str] = (
88
- FILE_SESSION_COMMANDS | TRANSFER_AND_LOCK_COMMANDS | UNIVERSAL_FILE_COMMANDS
89
- )
90
-
91
- # ``FileSessionClient`` method names for each ``FILE_SESSION_COMMANDS`` entry.
92
- # ``session_list_file_locks`` is also used by ``assert_session_exists``.
93
- FILE_SESSION_FACADE_METHODS: Dict[str, str] = {
94
- "session_create": "create_session",
95
- "session_delete": "delete_session",
96
- "session_list": "list_sessions",
97
- "session_view": "view_session",
98
- "session_open_file": "lock_file",
99
- "session_close_file": "unlock_file",
100
- "session_list_file_locks": "list_file_locks",
101
- "subordinate_session_create": "create_subordinate_session",
102
- "subordinate_session_get": "get_subordinate_session",
103
- "subordinate_session_update": "update_subordinate_session",
104
- "subordinate_session_delete": "delete_subordinate_session",
105
- "subordinate_session_list": "list_subordinate_sessions",
106
- }
107
-
108
-
109
- def assert_file_session_facade_complete() -> None:
110
- """Raise ``AssertionError`` if ``FILE_SESSION_FACADE_METHODS`` is incomplete."""
111
- missing_cmds = FILE_SESSION_COMMANDS - set(FILE_SESSION_FACADE_METHODS)
112
- extra_cmds = set(FILE_SESSION_FACADE_METHODS) - FILE_SESSION_COMMANDS
113
- if missing_cmds or extra_cmds:
114
- raise AssertionError(
115
- "FILE_SESSION_FACADE_METHODS out of sync with FILE_SESSION_COMMANDS: "
116
- f"missing={sorted(missing_cmds)!r} extra={sorted(extra_cmds)!r}"
117
- )
118
- from code_analysis_client.file_session import FileSessionClient
119
-
120
- for command, method_name in FILE_SESSION_FACADE_METHODS.items():
121
- if not hasattr(FileSessionClient, method_name):
122
- raise AssertionError(
123
- f"FileSessionClient missing method {method_name!r} for command {command!r}"
124
- )
125
-
126
-
127
- def _command_registered(get_command: Callable[[str], object], name: str) -> bool:
128
- try:
129
- get_command(name)
130
- return True
131
- except KeyError:
132
- return False
133
-
134
-
135
- def assert_facade_commands_registered(get_command: Callable[[str], object]) -> None:
136
- """Raise ``KeyError`` if any facade command is missing from the server registry."""
137
- missing = [
138
- name
139
- for name in sorted(CLIENT_FACADE_COMMANDS)
140
- if not _command_registered(get_command, name)
141
- ]
142
- if missing:
143
- raise KeyError(f"Client facade commands not registered on server: {missing}")
144
-
145
-
146
- def assert_removed_commands_absent(get_command: Callable[[str], object]) -> None:
147
- """Raise ``AssertionError`` if a removed command is still registered."""
148
- present = [
149
- name
150
- for name in sorted(REMOVED_COMMANDS)
151
- if _command_registered(get_command, name)
152
- ]
153
- if present:
154
- raise AssertionError(f"Removed commands still registered: {present}")