code-analysis-client 1.0.0__tar.gz → 1.0.1__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.0.0 → code_analysis_client-1.0.1}/PKG-INFO +13 -2
- {code_analysis_client-1.0.0 → code_analysis_client-1.0.1}/README.md +11 -0
- {code_analysis_client-1.0.0 → code_analysis_client-1.0.1}/code_analysis_client/__init__.py +4 -0
- {code_analysis_client-1.0.0 → code_analysis_client-1.0.1}/code_analysis_client/client.py +6 -0
- code_analysis_client-1.0.1/code_analysis_client/file_session.py +385 -0
- code_analysis_client-1.0.1/code_analysis_client/version.txt +1 -0
- {code_analysis_client-1.0.0 → code_analysis_client-1.0.1}/code_analysis_client.egg-info/PKG-INFO +13 -2
- {code_analysis_client-1.0.0 → code_analysis_client-1.0.1}/code_analysis_client.egg-info/SOURCES.txt +1 -0
- {code_analysis_client-1.0.0 → code_analysis_client-1.0.1}/code_analysis_client.egg-info/requires.txt +1 -1
- {code_analysis_client-1.0.0 → code_analysis_client-1.0.1}/pyproject.toml +1 -1
- code_analysis_client-1.0.0/code_analysis_client/version.txt +0 -1
- {code_analysis_client-1.0.0 → code_analysis_client-1.0.1}/code_analysis_client/commands_proxy.py +0 -0
- {code_analysis_client-1.0.0 → code_analysis_client-1.0.1}/code_analysis_client/config.py +0 -0
- {code_analysis_client-1.0.0 → code_analysis_client-1.0.1}/code_analysis_client/exceptions.py +0 -0
- {code_analysis_client-1.0.0 → code_analysis_client-1.0.1}/code_analysis_client/py.typed +0 -0
- {code_analysis_client-1.0.0 → code_analysis_client-1.0.1}/code_analysis_client/server_schema.py +0 -0
- {code_analysis_client-1.0.0 → code_analysis_client-1.0.1}/code_analysis_client/validation.py +0 -0
- {code_analysis_client-1.0.0 → code_analysis_client-1.0.1}/code_analysis_client.egg-info/dependency_links.txt +0 -0
- {code_analysis_client-1.0.0 → code_analysis_client-1.0.1}/code_analysis_client.egg-info/top_level.txt +0 -0
- {code_analysis_client-1.0.0 → code_analysis_client-1.0.1}/setup.cfg +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-analysis-client
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.1
|
|
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.13
|
|
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"
|
|
@@ -91,3 +91,14 @@ From the repository root:
|
|
|
91
91
|
pip install -e ./client
|
|
92
92
|
pytest tests/test_code_analysis_client.py
|
|
93
93
|
```
|
|
94
|
+
|
|
95
|
+
### Releasing to PyPI (version = root ``code-analysis`` project)
|
|
96
|
+
|
|
97
|
+
The client wheel version is read from ``client/code_analysis_client/version.txt``.
|
|
98
|
+
That file must match ``[project].version`` in the **repository root**
|
|
99
|
+
``pyproject.toml``. Sync before build:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
python scripts/sync_code_analysis_client_version.py
|
|
103
|
+
cd client && python -m build && twine check dist/* && twine upload dist/*
|
|
104
|
+
```
|
|
@@ -79,3 +79,14 @@ From the repository root:
|
|
|
79
79
|
pip install -e ./client
|
|
80
80
|
pytest tests/test_code_analysis_client.py
|
|
81
81
|
```
|
|
82
|
+
|
|
83
|
+
### Releasing to PyPI (version = root ``code-analysis`` project)
|
|
84
|
+
|
|
85
|
+
The client wheel version is read from ``client/code_analysis_client/version.txt``.
|
|
86
|
+
That file must match ``[project].version`` in the **repository root**
|
|
87
|
+
``pyproject.toml``. Sync before build:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
python scripts/sync_code_analysis_client_version.py
|
|
91
|
+
cd client && python -m build && twine check dist/* && twine upload dist/*
|
|
92
|
+
```
|
|
@@ -17,6 +17,7 @@ from code_analysis_client.config import (
|
|
|
17
17
|
load_server_config,
|
|
18
18
|
)
|
|
19
19
|
from code_analysis_client.exceptions import ClientValidationError
|
|
20
|
+
from code_analysis_client.file_session import FileSessionClient, SessionNotFoundError
|
|
20
21
|
from code_analysis_client.server_schema import (
|
|
21
22
|
fetch_command_schema_from_server,
|
|
22
23
|
parse_schema_from_help_payload,
|
|
@@ -29,6 +30,8 @@ from code_analysis_client.validation import (
|
|
|
29
30
|
__all__ = [
|
|
30
31
|
"ClientValidationError",
|
|
31
32
|
"CodeAnalysisAsyncClient",
|
|
33
|
+
"FileSessionClient",
|
|
34
|
+
"SessionNotFoundError",
|
|
32
35
|
"ValidatedCommandsProxy",
|
|
33
36
|
"adapter_settings_from_server_config",
|
|
34
37
|
"adapter_settings_to_jsonrpc_kwargs",
|
|
@@ -39,6 +42,7 @@ __all__ = [
|
|
|
39
42
|
"validate_params_against_schema",
|
|
40
43
|
]
|
|
41
44
|
|
|
45
|
+
|
|
42
46
|
def _read_package_version() -> str:
|
|
43
47
|
vf = Path(__file__).resolve().parent / "version.txt"
|
|
44
48
|
if vf.is_file():
|
|
@@ -21,6 +21,7 @@ from code_analysis_client.config import (
|
|
|
21
21
|
adapter_settings_to_jsonrpc_kwargs,
|
|
22
22
|
load_server_config,
|
|
23
23
|
)
|
|
24
|
+
from code_analysis_client.file_session import FileSessionClient
|
|
24
25
|
from code_analysis_client.server_schema import fetch_command_schema_from_server
|
|
25
26
|
from code_analysis_client.validation import (
|
|
26
27
|
prepare_params_for_schema,
|
|
@@ -119,6 +120,11 @@ class CodeAnalysisAsyncClient:
|
|
|
119
120
|
"""Drop cached command schemas (after ``reload`` or when server definitions change)."""
|
|
120
121
|
self._command_schema_cache.clear()
|
|
121
122
|
|
|
123
|
+
@property
|
|
124
|
+
def file_sessions(self) -> FileSessionClient:
|
|
125
|
+
"""Session-scoped download/upload and lock workflow (no CST/universal edit details)."""
|
|
126
|
+
return FileSessionClient(self)
|
|
127
|
+
|
|
122
128
|
async def get_command_schema(
|
|
123
129
|
self, command: str, *, refresh: bool = False
|
|
124
130
|
) -> Dict[str, Any]:
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
"""
|
|
2
|
+
High-level file transfer and session workflow on top of CodeAnalysisAsyncClient.
|
|
3
|
+
|
|
4
|
+
Hides transfer/lock command names behind a small API: create a client session,
|
|
5
|
+
download a project file with an advisory lock, upload changes with unlock, and
|
|
6
|
+
lock/unlock indexed files without moving bytes.
|
|
7
|
+
|
|
8
|
+
Author: Vasiliy Zdanovskiy
|
|
9
|
+
email: vasilyvz@gmail.com
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
|
|
16
|
+
|
|
17
|
+
from code_analysis_client.exceptions import ClientValidationError
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from code_analysis_client.client import CodeAnalysisAsyncClient
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SessionNotFoundError(ClientValidationError):
|
|
24
|
+
"""Raised when session_id is absent from client_sessions."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _unwrap(data: Dict[str, Any]) -> Dict[str, Any]:
|
|
28
|
+
if data.get("success"):
|
|
29
|
+
inner = data.get("data")
|
|
30
|
+
return inner if isinstance(inner, dict) else data
|
|
31
|
+
code_raw = data.get("code")
|
|
32
|
+
message = data.get("message")
|
|
33
|
+
err = data.get("error")
|
|
34
|
+
if code_raw is None and isinstance(err, dict):
|
|
35
|
+
code_raw = err.get("code")
|
|
36
|
+
if message is None:
|
|
37
|
+
message = err.get("message")
|
|
38
|
+
elif code_raw is None and isinstance(err, str):
|
|
39
|
+
code_raw = err
|
|
40
|
+
code = str(code_raw or "COMMAND_FAILED")
|
|
41
|
+
if message is None:
|
|
42
|
+
message = str(data)
|
|
43
|
+
if code == "SESSION_NOT_FOUND":
|
|
44
|
+
raise SessionNotFoundError(message, field="session_id", details=data)
|
|
45
|
+
raise ClientValidationError(message, field="command", details=data)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class FileSessionClient:
|
|
49
|
+
"""Session-scoped file download/upload and lock helpers."""
|
|
50
|
+
|
|
51
|
+
__slots__ = ("_client",)
|
|
52
|
+
|
|
53
|
+
def __init__(self, client: CodeAnalysisAsyncClient) -> None:
|
|
54
|
+
self._client = client
|
|
55
|
+
|
|
56
|
+
async def create_session(
|
|
57
|
+
self,
|
|
58
|
+
comment: str,
|
|
59
|
+
*,
|
|
60
|
+
role_ids: Optional[List[str]] = None,
|
|
61
|
+
) -> str:
|
|
62
|
+
"""Create a client session (``session_create``) and return ``session_id``."""
|
|
63
|
+
params: Dict[str, Any] = {"comment": comment}
|
|
64
|
+
if role_ids is not None:
|
|
65
|
+
params["role_ids"] = role_ids
|
|
66
|
+
payload = _unwrap(await self._client.call_validated("session_create", params))
|
|
67
|
+
sid = str(payload.get("session_id") or "").strip()
|
|
68
|
+
if not sid:
|
|
69
|
+
raise ClientValidationError(
|
|
70
|
+
"session_create returned no session_id",
|
|
71
|
+
field="session_id",
|
|
72
|
+
details=payload,
|
|
73
|
+
)
|
|
74
|
+
return sid
|
|
75
|
+
|
|
76
|
+
async def assert_session_exists(self, session_id: str) -> None:
|
|
77
|
+
"""Verify ``session_id`` is registered (touch via ``session_list_file_locks``)."""
|
|
78
|
+
sid = str(session_id).strip()
|
|
79
|
+
if not sid:
|
|
80
|
+
raise ClientValidationError("session_id is required", field="session_id")
|
|
81
|
+
_unwrap(
|
|
82
|
+
await self._client.call_validated(
|
|
83
|
+
"session_list_file_locks",
|
|
84
|
+
{"session_id": sid},
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
async def delete_session(
|
|
89
|
+
self, session_id: str, *, force: bool = False
|
|
90
|
+
) -> Dict[str, Any]:
|
|
91
|
+
"""Delete a client session (``session_delete``)."""
|
|
92
|
+
return _unwrap(
|
|
93
|
+
await self._client.call_validated(
|
|
94
|
+
"session_delete",
|
|
95
|
+
{"session_id": session_id, "force": force},
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
async def list_sessions(
|
|
100
|
+
self,
|
|
101
|
+
*,
|
|
102
|
+
session_id: Optional[str] = None,
|
|
103
|
+
stale_threshold_seconds: Optional[int] = None,
|
|
104
|
+
) -> Dict[str, Any]:
|
|
105
|
+
"""List client sessions (``session_list``)."""
|
|
106
|
+
params: Dict[str, Any] = {}
|
|
107
|
+
if session_id is not None:
|
|
108
|
+
params["session_id"] = session_id
|
|
109
|
+
if stale_threshold_seconds is not None:
|
|
110
|
+
params["stale_threshold_seconds"] = stale_threshold_seconds
|
|
111
|
+
return _unwrap(await self._client.call_validated("session_list", params))
|
|
112
|
+
|
|
113
|
+
async def lock_file(
|
|
114
|
+
self,
|
|
115
|
+
session_id: str,
|
|
116
|
+
project_id: str,
|
|
117
|
+
file_id: str,
|
|
118
|
+
) -> Dict[str, Any]:
|
|
119
|
+
"""Acquire a DB file lock without transfer (``session_open_file``)."""
|
|
120
|
+
await self.assert_session_exists(session_id)
|
|
121
|
+
return _unwrap(
|
|
122
|
+
await self._client.call_validated(
|
|
123
|
+
"session_open_file",
|
|
124
|
+
{
|
|
125
|
+
"session_id": session_id,
|
|
126
|
+
"project_id": project_id,
|
|
127
|
+
"file_id": file_id,
|
|
128
|
+
},
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
async def unlock_file(
|
|
133
|
+
self,
|
|
134
|
+
session_id: str,
|
|
135
|
+
project_id: str,
|
|
136
|
+
file_id: str,
|
|
137
|
+
) -> Dict[str, Any]:
|
|
138
|
+
"""Release a DB file lock without transfer (``session_close_file``)."""
|
|
139
|
+
await self.assert_session_exists(session_id)
|
|
140
|
+
return _unwrap(
|
|
141
|
+
await self._client.call_validated(
|
|
142
|
+
"session_close_file",
|
|
143
|
+
{
|
|
144
|
+
"session_id": session_id,
|
|
145
|
+
"project_id": project_id,
|
|
146
|
+
"file_id": file_id,
|
|
147
|
+
},
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
async def list_file_locks(self, session_id: str) -> Dict[str, Any]:
|
|
152
|
+
"""Return locks held by the session (``session_list_file_locks``)."""
|
|
153
|
+
return _unwrap(
|
|
154
|
+
await self._client.call_validated(
|
|
155
|
+
"session_list_file_locks",
|
|
156
|
+
{"session_id": session_id},
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
async def lock_files_advisory(
|
|
161
|
+
self,
|
|
162
|
+
session_id: str,
|
|
163
|
+
project_id: str,
|
|
164
|
+
file_paths: List[str],
|
|
165
|
+
*,
|
|
166
|
+
lock_mode: str = "full",
|
|
167
|
+
timeout_seconds: Optional[float] = None,
|
|
168
|
+
) -> Dict[str, Any]:
|
|
169
|
+
"""Cooperative flock + DB lease for paths (``project_file_advisory_lock_batch``)."""
|
|
170
|
+
await self.assert_session_exists(session_id)
|
|
171
|
+
items = [
|
|
172
|
+
{
|
|
173
|
+
"session_id": session_id,
|
|
174
|
+
"project_id": project_id,
|
|
175
|
+
"file_path": path,
|
|
176
|
+
"action": "lock",
|
|
177
|
+
"lock_mode": lock_mode,
|
|
178
|
+
}
|
|
179
|
+
for path in file_paths
|
|
180
|
+
]
|
|
181
|
+
params: Dict[str, Any] = {
|
|
182
|
+
"items": items,
|
|
183
|
+
"allow_foreign_session": True,
|
|
184
|
+
}
|
|
185
|
+
if timeout_seconds is not None:
|
|
186
|
+
params["timeout_seconds"] = timeout_seconds
|
|
187
|
+
return _unwrap(
|
|
188
|
+
await self._client.call_validated(
|
|
189
|
+
"project_file_advisory_lock_batch",
|
|
190
|
+
params,
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
async def unlock_files_advisory(
|
|
195
|
+
self,
|
|
196
|
+
session_id: str,
|
|
197
|
+
project_id: str,
|
|
198
|
+
file_paths: List[str],
|
|
199
|
+
) -> Dict[str, Any]:
|
|
200
|
+
"""Release cooperative locks without transfer."""
|
|
201
|
+
await self.assert_session_exists(session_id)
|
|
202
|
+
items = [
|
|
203
|
+
{
|
|
204
|
+
"session_id": session_id,
|
|
205
|
+
"project_id": project_id,
|
|
206
|
+
"file_path": path,
|
|
207
|
+
"action": "unlock",
|
|
208
|
+
}
|
|
209
|
+
for path in file_paths
|
|
210
|
+
]
|
|
211
|
+
return _unwrap(
|
|
212
|
+
await self._client.call_validated(
|
|
213
|
+
"project_file_advisory_lock_batch",
|
|
214
|
+
{
|
|
215
|
+
"items": items,
|
|
216
|
+
"allow_foreign_session": True,
|
|
217
|
+
},
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
async def begin_download_locked(
|
|
222
|
+
self,
|
|
223
|
+
session_id: str,
|
|
224
|
+
*,
|
|
225
|
+
project_id: Optional[str] = None,
|
|
226
|
+
file_id: Optional[str] = None,
|
|
227
|
+
file_path: Optional[str] = None,
|
|
228
|
+
compression: str = "identity",
|
|
229
|
+
lock_mode: str = "full",
|
|
230
|
+
chunk_size: Optional[int] = None,
|
|
231
|
+
include_backup_history: bool = False,
|
|
232
|
+
) -> Dict[str, Any]:
|
|
233
|
+
"""Start a chunked download and acquire an advisory lock (``project_file_transfer_download_begin``)."""
|
|
234
|
+
await self.assert_session_exists(session_id)
|
|
235
|
+
params: Dict[str, Any] = {
|
|
236
|
+
"session_id": session_id,
|
|
237
|
+
"compression": compression,
|
|
238
|
+
"lock_mode": lock_mode,
|
|
239
|
+
"include_backup_history": include_backup_history,
|
|
240
|
+
}
|
|
241
|
+
if project_id is not None:
|
|
242
|
+
params["project_id"] = project_id
|
|
243
|
+
if file_id is not None:
|
|
244
|
+
params["file_id"] = file_id
|
|
245
|
+
if file_path is not None:
|
|
246
|
+
params["file_path"] = file_path
|
|
247
|
+
if chunk_size is not None:
|
|
248
|
+
params["chunk_size"] = chunk_size
|
|
249
|
+
return _unwrap(
|
|
250
|
+
await self._client.call_validated(
|
|
251
|
+
"project_file_transfer_download_begin",
|
|
252
|
+
params,
|
|
253
|
+
)
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
async def download_to_path(
|
|
257
|
+
self,
|
|
258
|
+
transfer_id: str,
|
|
259
|
+
destination: Union[str, Path],
|
|
260
|
+
) -> Any:
|
|
261
|
+
"""Stream download chunks to ``destination`` via the adapter client."""
|
|
262
|
+
return await self._client.rpc.download_file(
|
|
263
|
+
str(transfer_id).strip(),
|
|
264
|
+
str(destination),
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
async def download_file_locked(
|
|
268
|
+
self,
|
|
269
|
+
session_id: str,
|
|
270
|
+
destination: Union[str, Path],
|
|
271
|
+
*,
|
|
272
|
+
project_id: Optional[str] = None,
|
|
273
|
+
file_id: Optional[str] = None,
|
|
274
|
+
file_path: Optional[str] = None,
|
|
275
|
+
compression: str = "identity",
|
|
276
|
+
lock_mode: str = "full",
|
|
277
|
+
) -> Tuple[Dict[str, Any], Any]:
|
|
278
|
+
"""Download + lock in one workflow; returns (begin_payload, download_receipt)."""
|
|
279
|
+
begin = await self.begin_download_locked(
|
|
280
|
+
session_id,
|
|
281
|
+
project_id=project_id,
|
|
282
|
+
file_id=file_id,
|
|
283
|
+
file_path=file_path,
|
|
284
|
+
compression=compression,
|
|
285
|
+
lock_mode=lock_mode,
|
|
286
|
+
)
|
|
287
|
+
transfer_id = str(begin.get("transfer_id") or "").strip()
|
|
288
|
+
if not transfer_id:
|
|
289
|
+
raise ClientValidationError(
|
|
290
|
+
"download begin returned no transfer_id",
|
|
291
|
+
field="transfer_id",
|
|
292
|
+
details=begin,
|
|
293
|
+
)
|
|
294
|
+
receipt = await self.download_to_path(transfer_id, destination)
|
|
295
|
+
return begin, receipt
|
|
296
|
+
|
|
297
|
+
async def upload_bytes(
|
|
298
|
+
self,
|
|
299
|
+
payload: bytes,
|
|
300
|
+
*,
|
|
301
|
+
filename: str,
|
|
302
|
+
compression: str = "identity",
|
|
303
|
+
) -> Any:
|
|
304
|
+
"""Upload raw bytes through the adapter buffer; returns upload receipt."""
|
|
305
|
+
dest = Path(filename)
|
|
306
|
+
dest.write_bytes(payload)
|
|
307
|
+
try:
|
|
308
|
+
return await self._client.rpc.upload_file(
|
|
309
|
+
str(dest),
|
|
310
|
+
filename=filename,
|
|
311
|
+
compression=compression,
|
|
312
|
+
)
|
|
313
|
+
finally:
|
|
314
|
+
if dest.is_file():
|
|
315
|
+
dest.unlink(missing_ok=True)
|
|
316
|
+
|
|
317
|
+
async def save_upload_and_unlock(
|
|
318
|
+
self,
|
|
319
|
+
session_id: str,
|
|
320
|
+
transfer_id: str,
|
|
321
|
+
*,
|
|
322
|
+
project_id: Optional[str] = None,
|
|
323
|
+
file_id: Optional[str] = None,
|
|
324
|
+
file_path: Optional[str] = None,
|
|
325
|
+
unlock_after_write: bool = True,
|
|
326
|
+
dry_run: bool = False,
|
|
327
|
+
backup: bool = True,
|
|
328
|
+
) -> Dict[str, Any]:
|
|
329
|
+
"""Persist uploaded buffer and release locks (``project_file_transfer_upload_save``)."""
|
|
330
|
+
await self.assert_session_exists(session_id)
|
|
331
|
+
params: Dict[str, Any] = {
|
|
332
|
+
"session_id": session_id,
|
|
333
|
+
"transfer_id": transfer_id,
|
|
334
|
+
"unlock_after_write": unlock_after_write,
|
|
335
|
+
"dry_run": dry_run,
|
|
336
|
+
"backup": backup,
|
|
337
|
+
}
|
|
338
|
+
if project_id is not None:
|
|
339
|
+
params["project_id"] = project_id
|
|
340
|
+
if file_id is not None:
|
|
341
|
+
params["file_id"] = file_id
|
|
342
|
+
if file_path is not None:
|
|
343
|
+
params["file_path"] = file_path
|
|
344
|
+
return _unwrap(
|
|
345
|
+
await self._client.call_validated(
|
|
346
|
+
"project_file_transfer_upload_save",
|
|
347
|
+
params,
|
|
348
|
+
)
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
async def upload_file_and_unlock(
|
|
352
|
+
self,
|
|
353
|
+
session_id: str,
|
|
354
|
+
payload: bytes,
|
|
355
|
+
*,
|
|
356
|
+
project_id: Optional[str] = None,
|
|
357
|
+
file_id: Optional[str] = None,
|
|
358
|
+
file_path: Optional[str] = None,
|
|
359
|
+
filename: Optional[str] = None,
|
|
360
|
+
compression: str = "identity",
|
|
361
|
+
unlock_after_write: bool = True,
|
|
362
|
+
backup: bool = True,
|
|
363
|
+
) -> Dict[str, Any]:
|
|
364
|
+
"""Upload bytes then save to the project file and unlock."""
|
|
365
|
+
name = filename or (Path(file_path).name if file_path else "payload.bin")
|
|
366
|
+
receipt = await self.upload_bytes(
|
|
367
|
+
payload,
|
|
368
|
+
filename=name,
|
|
369
|
+
compression=compression,
|
|
370
|
+
)
|
|
371
|
+
if not getattr(receipt, "completed", False):
|
|
372
|
+
raise ClientValidationError(
|
|
373
|
+
"upload did not complete",
|
|
374
|
+
field="transfer_id",
|
|
375
|
+
details={"receipt": repr(receipt)},
|
|
376
|
+
)
|
|
377
|
+
return await self.save_upload_and_unlock(
|
|
378
|
+
session_id,
|
|
379
|
+
str(receipt.transfer_id),
|
|
380
|
+
project_id=project_id,
|
|
381
|
+
file_id=file_id,
|
|
382
|
+
file_path=file_path,
|
|
383
|
+
unlock_after_write=unlock_after_write,
|
|
384
|
+
backup=backup,
|
|
385
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.0.1
|
{code_analysis_client-1.0.0 → code_analysis_client-1.0.1}/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.0.
|
|
3
|
+
Version: 1.0.1
|
|
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.13
|
|
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"
|
|
@@ -91,3 +91,14 @@ From the repository root:
|
|
|
91
91
|
pip install -e ./client
|
|
92
92
|
pytest tests/test_code_analysis_client.py
|
|
93
93
|
```
|
|
94
|
+
|
|
95
|
+
### Releasing to PyPI (version = root ``code-analysis`` project)
|
|
96
|
+
|
|
97
|
+
The client wheel version is read from ``client/code_analysis_client/version.txt``.
|
|
98
|
+
That file must match ``[project].version`` in the **repository root**
|
|
99
|
+
``pyproject.toml``. Sync before build:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
python scripts/sync_code_analysis_client_version.py
|
|
103
|
+
cd client && python -m build && twine check dist/* && twine upload dist/*
|
|
104
|
+
```
|
{code_analysis_client-1.0.0 → code_analysis_client-1.0.1}/code_analysis_client.egg-info/SOURCES.txt
RENAMED
|
@@ -5,6 +5,7 @@ code_analysis_client/client.py
|
|
|
5
5
|
code_analysis_client/commands_proxy.py
|
|
6
6
|
code_analysis_client/config.py
|
|
7
7
|
code_analysis_client/exceptions.py
|
|
8
|
+
code_analysis_client/file_session.py
|
|
8
9
|
code_analysis_client/py.typed
|
|
9
10
|
code_analysis_client/server_schema.py
|
|
10
11
|
code_analysis_client/validation.py
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
1.0.0
|
{code_analysis_client-1.0.0 → code_analysis_client-1.0.1}/code_analysis_client/commands_proxy.py
RENAMED
|
File without changes
|
|
File without changes
|
{code_analysis_client-1.0.0 → code_analysis_client-1.0.1}/code_analysis_client/exceptions.py
RENAMED
|
File without changes
|
|
File without changes
|
{code_analysis_client-1.0.0 → code_analysis_client-1.0.1}/code_analysis_client/server_schema.py
RENAMED
|
File without changes
|
{code_analysis_client-1.0.0 → code_analysis_client-1.0.1}/code_analysis_client/validation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|