code-analysis-client 1.0.6__tar.gz → 1.0.9__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 (23) hide show
  1. {code_analysis_client-1.0.6 → code_analysis_client-1.0.9}/PKG-INFO +1 -1
  2. {code_analysis_client-1.0.6 → code_analysis_client-1.0.9}/code_analysis_client/__init__.py +2 -0
  3. {code_analysis_client-1.0.6 → code_analysis_client-1.0.9}/code_analysis_client/file_session.py +121 -37
  4. {code_analysis_client-1.0.6 → code_analysis_client-1.0.9}/code_analysis_client/server_api.py +31 -1
  5. code_analysis_client-1.0.9/code_analysis_client/version.txt +1 -0
  6. {code_analysis_client-1.0.6 → code_analysis_client-1.0.9}/code_analysis_client.egg-info/PKG-INFO +1 -1
  7. code_analysis_client-1.0.6/code_analysis_client/version.txt +0 -1
  8. {code_analysis_client-1.0.6 → code_analysis_client-1.0.9}/README.md +0 -0
  9. {code_analysis_client-1.0.6 → code_analysis_client-1.0.9}/code_analysis_client/client.py +0 -0
  10. {code_analysis_client-1.0.6 → code_analysis_client-1.0.9}/code_analysis_client/commands_proxy.py +0 -0
  11. {code_analysis_client-1.0.6 → code_analysis_client-1.0.9}/code_analysis_client/config.py +0 -0
  12. {code_analysis_client-1.0.6 → code_analysis_client-1.0.9}/code_analysis_client/exceptions.py +0 -0
  13. {code_analysis_client-1.0.6 → code_analysis_client-1.0.9}/code_analysis_client/py.typed +0 -0
  14. {code_analysis_client-1.0.6 → code_analysis_client-1.0.9}/code_analysis_client/responses.py +0 -0
  15. {code_analysis_client-1.0.6 → code_analysis_client-1.0.9}/code_analysis_client/server_schema.py +0 -0
  16. {code_analysis_client-1.0.6 → code_analysis_client-1.0.9}/code_analysis_client/universal_file.py +0 -0
  17. {code_analysis_client-1.0.6 → code_analysis_client-1.0.9}/code_analysis_client/validation.py +0 -0
  18. {code_analysis_client-1.0.6 → code_analysis_client-1.0.9}/code_analysis_client.egg-info/SOURCES.txt +0 -0
  19. {code_analysis_client-1.0.6 → code_analysis_client-1.0.9}/code_analysis_client.egg-info/dependency_links.txt +0 -0
  20. {code_analysis_client-1.0.6 → code_analysis_client-1.0.9}/code_analysis_client.egg-info/requires.txt +0 -0
  21. {code_analysis_client-1.0.6 → code_analysis_client-1.0.9}/code_analysis_client.egg-info/top_level.txt +0 -0
  22. {code_analysis_client-1.0.6 → code_analysis_client-1.0.9}/pyproject.toml +0 -0
  23. {code_analysis_client-1.0.6 → code_analysis_client-1.0.9}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-analysis-client
3
- Version: 1.0.6
3
+ Version: 1.0.9
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
@@ -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",
@@ -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
  """
@@ -39,12 +54,41 @@ def _require_non_empty(value: str, *, field: str) -> str:
39
54
 
40
55
 
41
56
  def _validate_download_target(*, file_id: Optional[str]) -> None:
42
- """Download requires ``file_id``."""
57
+ """Download requires ``file_id`` (server rejects ``file_path``)."""
43
58
  fid = str(file_id or "").strip()
44
59
  if not fid:
45
60
  raise ClientValidationError("file_id is required", field="file_id")
46
61
 
47
62
 
63
+ def _validate_upload_selector(
64
+ *,
65
+ file_id: Optional[str] = None,
66
+ file_path: Optional[str] = None,
67
+ project_id: Optional[str] = None,
68
+ ) -> None:
69
+ """Mirror server ``_validate_upload_selector_params`` for save payloads."""
70
+ fid = str(file_id or "").strip()
71
+ fp = str(file_path or "").strip()
72
+ pid = str(project_id or "").strip()
73
+ if fid and fp:
74
+ raise ClientValidationError(
75
+ "Specify exactly one of file_id or file_path, not both",
76
+ field="file_id",
77
+ )
78
+ if fid:
79
+ return
80
+ if not pid:
81
+ raise ClientValidationError(
82
+ "project_id is required when file_id is omitted",
83
+ field="project_id",
84
+ )
85
+ if not fp:
86
+ raise ClientValidationError(
87
+ "file_path is required when file_id is omitted",
88
+ field="file_path",
89
+ )
90
+
91
+
48
92
  def _lock_mode_from_flag(lock: bool) -> str:
49
93
  return "full" if lock else "none"
50
94
 
@@ -109,7 +153,8 @@ class FileSessionClient:
109
153
  ``force`` defaults to false (omit on the wire when false). Safe delete
110
154
  requires no ``session_file_locks`` and no ``subordinate_sessions`` rows
111
155
  where this session is ``parent_session_id``. When ``force`` is true,
112
- linked subordinate client sessions and file locks are removed first.
156
+ subordinate server link rows and file locks for this session are removed
157
+ before the session row is deleted.
113
158
 
114
159
  Returns ``session_id``, ``deleted``, ``released_lock_count``, and
115
160
  ``released_subordinate_count``.
@@ -129,8 +174,7 @@ class FileSessionClient:
129
174
 
130
175
  Returns ``locked_files_by_project`` (file_id, file_path,
131
176
  project_presentation per project) and ``subordinate_sessions``
132
- (subordinate_session_id, server_uuid, session_presentation,
133
- server_presentation, link_comment).
177
+ (server_uuid, session_presentation, server_presentation, link_comment).
134
178
  """
135
179
  sid = _require_non_empty(session_id, field="session_id")
136
180
  return _unwrap(
@@ -143,23 +187,19 @@ class FileSessionClient:
143
187
  async def create_subordinate_session(
144
188
  self,
145
189
  parent_session_id: str,
146
- subordinate_session_id: str,
147
190
  comment: str,
148
191
  *,
149
192
  server_uuid: Optional[str] = None,
150
193
  ) -> Dict[str, Any]:
151
- """Link subordinate to parent (``subordinate_session_create``).
194
+ """Register leading session on a subordinate server (``subordinate_session_create``).
152
195
 
153
- ``server_uuid`` defaults to the server's ``registration.instance_uuid``
154
- when omitted.
196
+ The same ``parent_session_id`` is used on the remote server. ``server_uuid``
197
+ defaults to the server's ``registration.instance_uuid`` when omitted.
155
198
  """
156
199
  params: Dict[str, Any] = {
157
200
  "parent_session_id": _require_non_empty(
158
201
  parent_session_id, field="parent_session_id"
159
202
  ),
160
- "subordinate_session_id": _require_non_empty(
161
- subordinate_session_id, field="subordinate_session_id"
162
- ),
163
203
  "comment": comment,
164
204
  }
165
205
  if server_uuid is not None:
@@ -171,7 +211,6 @@ class FileSessionClient:
171
211
  async def get_subordinate_session(
172
212
  self,
173
213
  parent_session_id: str,
174
- subordinate_session_id: str,
175
214
  server_uuid: str,
176
215
  ) -> Dict[str, Any]:
177
216
  """Fetch one subordinate link (``subordinate_session_get``)."""
@@ -182,9 +221,6 @@ class FileSessionClient:
182
221
  "parent_session_id": _require_non_empty(
183
222
  parent_session_id, field="parent_session_id"
184
223
  ),
185
- "subordinate_session_id": _require_non_empty(
186
- subordinate_session_id, field="subordinate_session_id"
187
- ),
188
224
  "server_uuid": _require_non_empty(server_uuid, field="server_uuid"),
189
225
  },
190
226
  )
@@ -193,7 +229,6 @@ class FileSessionClient:
193
229
  async def update_subordinate_session(
194
230
  self,
195
231
  parent_session_id: str,
196
- subordinate_session_id: str,
197
232
  server_uuid: str,
198
233
  comment: str,
199
234
  ) -> Dict[str, Any]:
@@ -205,9 +240,6 @@ class FileSessionClient:
205
240
  "parent_session_id": _require_non_empty(
206
241
  parent_session_id, field="parent_session_id"
207
242
  ),
208
- "subordinate_session_id": _require_non_empty(
209
- subordinate_session_id, field="subordinate_session_id"
210
- ),
211
243
  "server_uuid": _require_non_empty(server_uuid, field="server_uuid"),
212
244
  "comment": comment,
213
245
  },
@@ -217,12 +249,11 @@ class FileSessionClient:
217
249
  async def delete_subordinate_session(
218
250
  self,
219
251
  parent_session_id: str,
220
- subordinate_session_id: str,
221
252
  server_uuid: str,
222
253
  ) -> Dict[str, Any]:
223
254
  """Remove subordinate link only (``subordinate_session_delete``).
224
255
 
225
- Does not delete ``client_sessions`` rows; use :meth:`delete_session` for that.
256
+ Does not delete the ``client_sessions`` row for ``parent_session_id``.
226
257
  """
227
258
  return _unwrap(
228
259
  await self._client.call_validated(
@@ -231,9 +262,6 @@ class FileSessionClient:
231
262
  "parent_session_id": _require_non_empty(
232
263
  parent_session_id, field="parent_session_id"
233
264
  ),
234
- "subordinate_session_id": _require_non_empty(
235
- subordinate_session_id, field="subordinate_session_id"
236
- ),
237
265
  "server_uuid": _require_non_empty(server_uuid, field="server_uuid"),
238
266
  },
239
267
  )
@@ -243,7 +271,6 @@ class FileSessionClient:
243
271
  self,
244
272
  *,
245
273
  parent_session_id: Optional[str] = None,
246
- subordinate_session_id: Optional[str] = None,
247
274
  server_uuid: Optional[str] = None,
248
275
  ) -> Dict[str, Any]:
249
276
  """List subordinate links (``subordinate_session_list``).
@@ -253,8 +280,6 @@ class FileSessionClient:
253
280
  params: Dict[str, Any] = {}
254
281
  if parent_session_id is not None:
255
282
  params["parent_session_id"] = parent_session_id
256
- if subordinate_session_id is not None:
257
- params["subordinate_session_id"] = subordinate_session_id
258
283
  if server_uuid is not None:
259
284
  params["server_uuid"] = server_uuid
260
285
  return _unwrap(
@@ -391,9 +416,10 @@ class FileSessionClient:
391
416
  compression: str = "identity",
392
417
  lock: bool = True,
393
418
  chunk_size: Optional[int] = None,
394
- include_backup_history: bool = False,
419
+ include_backup_history: bool = True,
420
+ project_id: Optional[str] = None,
395
421
  ) -> Dict[str, Any]:
396
- """Begin chunked download; returns payload including ``file_id`` and ``transfer_id``."""
422
+ """Internal: call ``project_file_transfer_download_begin``."""
397
423
  fid = _require_non_empty(file_id, field="file_id")
398
424
  _validate_download_target(file_id=fid)
399
425
  await self.assert_session_exists(session_id)
@@ -406,6 +432,8 @@ class FileSessionClient:
406
432
  }
407
433
  if chunk_size is not None:
408
434
  params["chunk_size"] = chunk_size
435
+ if project_id is not None:
436
+ params["project_id"] = _require_non_empty(project_id, field="project_id")
409
437
  payload = _unwrap(
410
438
  await self._client.call_validated(
411
439
  "project_file_transfer_download_begin",
@@ -423,9 +451,14 @@ class FileSessionClient:
423
451
  *,
424
452
  compression: str = "identity",
425
453
  lock: bool = True,
426
- include_backup_history: bool = False,
454
+ include_backup_history: bool = True,
455
+ project_id: Optional[str] = None,
427
456
  ) -> Tuple[Dict[str, Any], Any]:
428
- """Download an indexed file by ``file_id``; returns (begin_payload, receipt)."""
457
+ """Download an indexed file by ``file_id`` (``project_file_transfer_download_begin``).
458
+
459
+ Returns ``(begin_payload, adapter_receipt)``. ``file_path`` is not accepted —
460
+ resolve ``file_id`` from ``list_project_files`` or ``session_view`` first.
461
+ """
429
462
  fid = _require_non_empty(file_id, field="file_id")
430
463
  begin = await self._begin_download(
431
464
  session_id,
@@ -433,6 +466,7 @@ class FileSessionClient:
433
466
  compression=compression,
434
467
  lock=lock,
435
468
  include_backup_history=include_backup_history,
469
+ project_id=project_id,
436
470
  )
437
471
  transfer_id = str(begin.get("transfer_id") or "").strip()
438
472
  if not transfer_id:
@@ -486,8 +520,19 @@ class FileSessionClient:
486
520
  unlock_after_write: bool = True,
487
521
  dry_run: bool = False,
488
522
  backup: bool = True,
523
+ diff: bool = False,
524
+ diff_context_lines: Optional[int] = None,
525
+ commit_message: Optional[str] = None,
526
+ validate_syntax_only: bool = False,
527
+ tree_id: Optional[str] = None,
528
+ lock_mode: Optional[str] = None,
489
529
  ) -> Dict[str, Any]:
490
530
  """Persist a completed adapter upload (``project_file_transfer_upload_save``)."""
531
+ _validate_upload_selector(
532
+ file_id=file_id,
533
+ file_path=file_path,
534
+ project_id=project_id,
535
+ )
491
536
  await self.assert_session_exists(session_id)
492
537
  params: Dict[str, Any] = {
493
538
  "session_id": session_id,
@@ -502,6 +547,18 @@ class FileSessionClient:
502
547
  params["file_id"] = file_id
503
548
  if file_path is not None:
504
549
  params["file_path"] = file_path
550
+ if diff:
551
+ params["diff"] = True
552
+ if diff_context_lines is not None:
553
+ params["diff_context_lines"] = diff_context_lines
554
+ if commit_message is not None:
555
+ params["commit_message"] = commit_message
556
+ if validate_syntax_only:
557
+ params["validate_syntax_only"] = True
558
+ if tree_id is not None:
559
+ params["tree_id"] = tree_id
560
+ if lock_mode is not None:
561
+ params["lock_mode"] = lock_mode
505
562
  return _unwrap(
506
563
  await self._client.call_validated(
507
564
  "project_file_transfer_upload_save",
@@ -521,11 +578,18 @@ class FileSessionClient:
521
578
  unlock: bool = True,
522
579
  dry_run: bool = False,
523
580
  backup: bool = True,
581
+ diff: bool = False,
582
+ diff_context_lines: Optional[int] = None,
583
+ commit_message: Optional[str] = None,
584
+ validate_syntax_only: bool = False,
585
+ tree_id: Optional[str] = None,
586
+ lock_mode: Optional[str] = None,
524
587
  ) -> str:
525
- """Create a new project file from uploaded bytes; returns the new ``file_id``.
588
+ """Create a new project file (``project_file_transfer_upload_save`` create mode).
526
589
 
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.
590
+ Pass ``project_id`` and project-relative ``file_path`` only do not pass
591
+ ``file_id``. The path must not already be indexed (``FILE_ALREADY_INDEXED``
592
+ otherwise). Returns the new ``file_id`` (may be empty on ``dry_run``).
529
593
  """
530
594
  pid = _require_non_empty(project_id, field="project_id")
531
595
  fp = _require_non_empty(file_path, field="file_path")
@@ -549,6 +613,12 @@ class FileSessionClient:
549
613
  unlock_after_write=unlock,
550
614
  backup=backup,
551
615
  dry_run=dry_run,
616
+ diff=diff,
617
+ diff_context_lines=diff_context_lines,
618
+ commit_message=commit_message,
619
+ validate_syntax_only=validate_syntax_only,
620
+ tree_id=tree_id,
621
+ lock_mode=lock_mode,
552
622
  )
553
623
  if dry_run:
554
624
  fid = str(saved.get("file_id") or "").strip()
@@ -561,16 +631,23 @@ class FileSessionClient:
561
631
  payload: bytes,
562
632
  file_id: str,
563
633
  *,
634
+ project_id: Optional[str] = None,
564
635
  filename: Optional[str] = None,
565
636
  compression: str = "identity",
566
637
  unlock: bool = True,
567
638
  backup: bool = True,
568
639
  dry_run: bool = False,
640
+ diff: bool = False,
641
+ diff_context_lines: Optional[int] = None,
642
+ commit_message: Optional[str] = None,
643
+ validate_syntax_only: bool = False,
644
+ tree_id: Optional[str] = None,
645
+ lock_mode: Optional[str] = None,
569
646
  ) -> Dict[str, Any]:
570
- """Overwrite an existing indexed file from uploaded bytes.
647
+ """Overwrite an existing indexed file (``project_file_transfer_upload_save`` update mode).
571
648
 
572
- Only ``file_id`` is required. Returns the server save payload on success
573
- or raises :class:`ClientValidationError` on failure.
649
+ Pass ``file_id`` only. Optional ``project_id`` must match the row if provided.
650
+ Do not pass ``file_path``. Returns the server save payload.
574
651
  """
575
652
  fid = _require_non_empty(file_id, field="file_id")
576
653
  name = filename or "payload.bin"
@@ -589,9 +666,16 @@ class FileSessionClient:
589
666
  session_id,
590
667
  str(receipt.transfer_id),
591
668
  file_id=fid,
669
+ project_id=project_id,
592
670
  unlock_after_write=unlock,
593
671
  backup=backup,
594
672
  dry_run=dry_run,
673
+ diff=diff,
674
+ diff_context_lines=diff_context_lines,
675
+ commit_message=commit_message,
676
+ validate_syntax_only=validate_syntax_only,
677
+ tree_id=tree_id,
678
+ lock_mode=lock_mode,
595
679
  )
596
680
  if not dry_run:
597
681
  _extract_file_id(saved)
@@ -12,7 +12,7 @@ email: vasilyvz@gmail.com
12
12
 
13
13
  from __future__ import annotations
14
14
 
15
- from typing import Callable, Dict, FrozenSet
15
+ from typing import Callable, Dict, FrozenSet, Tuple
16
16
 
17
17
  # Removed from the public server surface — do not use in client facades or docs.
18
18
  LEGACY_REMOVED_COMMANDS: FrozenSet[str] = frozenset(
@@ -105,6 +105,16 @@ FILE_SESSION_FACADE_METHODS: Dict[str, str] = {
105
105
  "subordinate_session_list": "list_subordinate_sessions",
106
106
  }
107
107
 
108
+ # Transfer / advisory-lock commands may map to more than one façade method.
109
+ TRANSFER_FACADE_METHODS: Dict[str, Tuple[str, ...]] = {
110
+ "project_file_transfer_download_begin": ("download",),
111
+ "project_file_transfer_upload_save": ("upload", "upload_new"),
112
+ "project_file_advisory_lock_batch": (
113
+ "lock_files_advisory",
114
+ "unlock_files_advisory",
115
+ ),
116
+ }
117
+
108
118
 
109
119
  def assert_file_session_facade_complete() -> None:
110
120
  """Raise ``AssertionError`` if ``FILE_SESSION_FACADE_METHODS`` is incomplete."""
@@ -124,6 +134,26 @@ def assert_file_session_facade_complete() -> None:
124
134
  )
125
135
 
126
136
 
137
+ def assert_transfer_facade_complete() -> None:
138
+ """Raise ``AssertionError`` if ``TRANSFER_FACADE_METHODS`` is incomplete."""
139
+ missing_cmds = TRANSFER_AND_LOCK_COMMANDS - set(TRANSFER_FACADE_METHODS)
140
+ extra_cmds = set(TRANSFER_FACADE_METHODS) - TRANSFER_AND_LOCK_COMMANDS
141
+ if missing_cmds or extra_cmds:
142
+ raise AssertionError(
143
+ "TRANSFER_FACADE_METHODS out of sync with TRANSFER_AND_LOCK_COMMANDS: "
144
+ f"missing={sorted(missing_cmds)!r} extra={sorted(extra_cmds)!r}"
145
+ )
146
+ from code_analysis_client.file_session import FileSessionClient
147
+
148
+ for command, method_names in TRANSFER_FACADE_METHODS.items():
149
+ for method_name in method_names:
150
+ if not hasattr(FileSessionClient, method_name):
151
+ raise AssertionError(
152
+ f"FileSessionClient missing method {method_name!r} "
153
+ f"for command {command!r}"
154
+ )
155
+
156
+
127
157
  def _command_registered(get_command: Callable[[str], object], name: str) -> bool:
128
158
  try:
129
159
  get_command(name)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-analysis-client
3
- Version: 1.0.6
3
+ Version: 1.0.9
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