code-analysis-client 1.0.5__tar.gz → 1.0.6__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.5 → code_analysis_client-1.0.6}/PKG-INFO +25 -11
- {code_analysis_client-1.0.5 → code_analysis_client-1.0.6}/README.md +24 -10
- {code_analysis_client-1.0.5 → code_analysis_client-1.0.6}/code_analysis_client/__init__.py +4 -0
- {code_analysis_client-1.0.5 → code_analysis_client-1.0.6}/code_analysis_client/client.py +1 -1
- {code_analysis_client-1.0.5 → code_analysis_client-1.0.6}/code_analysis_client/file_session.py +267 -58
- {code_analysis_client-1.0.5 → code_analysis_client-1.0.6}/code_analysis_client/server_api.py +42 -1
- code_analysis_client-1.0.6/code_analysis_client/version.txt +1 -0
- {code_analysis_client-1.0.5 → code_analysis_client-1.0.6}/code_analysis_client.egg-info/PKG-INFO +25 -11
- code_analysis_client-1.0.5/code_analysis_client/version.txt +0 -1
- {code_analysis_client-1.0.5 → code_analysis_client-1.0.6}/code_analysis_client/commands_proxy.py +0 -0
- {code_analysis_client-1.0.5 → code_analysis_client-1.0.6}/code_analysis_client/config.py +0 -0
- {code_analysis_client-1.0.5 → code_analysis_client-1.0.6}/code_analysis_client/exceptions.py +0 -0
- {code_analysis_client-1.0.5 → code_analysis_client-1.0.6}/code_analysis_client/py.typed +0 -0
- {code_analysis_client-1.0.5 → code_analysis_client-1.0.6}/code_analysis_client/responses.py +0 -0
- {code_analysis_client-1.0.5 → code_analysis_client-1.0.6}/code_analysis_client/server_schema.py +0 -0
- {code_analysis_client-1.0.5 → code_analysis_client-1.0.6}/code_analysis_client/universal_file.py +0 -0
- {code_analysis_client-1.0.5 → code_analysis_client-1.0.6}/code_analysis_client/validation.py +0 -0
- {code_analysis_client-1.0.5 → code_analysis_client-1.0.6}/code_analysis_client.egg-info/SOURCES.txt +0 -0
- {code_analysis_client-1.0.5 → code_analysis_client-1.0.6}/code_analysis_client.egg-info/dependency_links.txt +0 -0
- {code_analysis_client-1.0.5 → code_analysis_client-1.0.6}/code_analysis_client.egg-info/requires.txt +0 -0
- {code_analysis_client-1.0.5 → code_analysis_client-1.0.6}/code_analysis_client.egg-info/top_level.txt +0 -0
- {code_analysis_client-1.0.5 → code_analysis_client-1.0.6}/pyproject.toml +0 -0
- {code_analysis_client-1.0.5 → code_analysis_client-1.0.6}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-analysis-client
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.6
|
|
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
|
|
@@ -75,7 +75,7 @@ async with CodeAnalysisAsyncClient(host="127.0.0.1", port=15001) as client:
|
|
|
75
75
|
|
|
76
76
|
Use `call_unified_validated` when you need queue polling. Pass `refresh_schema=True` on a single call to bypass the in-memory schema cache.
|
|
77
77
|
|
|
78
|
-
## High-level facades (aligned with server
|
|
78
|
+
## High-level facades (aligned with live server registry)
|
|
79
79
|
|
|
80
80
|
The client does **not** wrap CST commands (`cst_load_file`, …) or legacy file I/O
|
|
81
81
|
(`universal_file_read`, `read_project_text_file`, …). Those commands are removed
|
|
@@ -83,28 +83,42 @@ from the server registry. Use the facades below or generic `call` / `commands.*`
|
|
|
83
83
|
|
|
84
84
|
| Facade | Property | Server commands |
|
|
85
85
|
|--------|----------|-----------------|
|
|
86
|
-
| Client DB sessions + transfer | `client.file_sessions` | `session_*`, `project_file_transfer_*`, `project_file_advisory_lock_batch` |
|
|
86
|
+
| Client DB sessions + transfer | `client.file_sessions` | `session_*`, `subordinate_session_*`, `project_file_transfer_*`, `project_file_advisory_lock_batch` |
|
|
87
87
|
| Universal edit sessions | `client.universal_files` | `universal_file_open`, `edit`, `write`, `close`, `preview` |
|
|
88
88
|
| Any registered command | `client.call` / `client.commands.<name>` | schema from live `help()` |
|
|
89
89
|
|
|
90
|
-
Canonical command lists: `code_analysis_client.server_api`
|
|
91
|
-
`
|
|
90
|
+
Canonical command lists: `code_analysis_client.server_api` — exported as
|
|
91
|
+
`FILE_SESSION_COMMANDS`, `FILE_SESSION_FACADE_METHODS`, `CLIENT_FACADE_COMMANDS`,
|
|
92
|
+
`REMOVED_COMMANDS`.
|
|
92
93
|
|
|
93
|
-
Sync
|
|
94
|
+
Sync checks (in-process registry):
|
|
94
95
|
|
|
95
96
|
```bash
|
|
96
|
-
pytest tests/test_client_server_api_sync.py
|
|
97
|
+
pytest tests/test_client_server_api_sync.py tests/test_code_analysis_client.py -k session
|
|
97
98
|
```
|
|
98
99
|
|
|
99
|
-
Package version
|
|
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
102
|
|
|
101
103
|
## Examples (this repository)
|
|
102
104
|
|
|
103
105
|
Runnable scripts live under `client/examples/`. **Long-form “man page” style
|
|
104
106
|
documentation** is embedded in the **module docstrings** of those Python files
|
|
105
|
-
(see `client/examples/README.md` for how to read them).
|
|
106
|
-
|
|
107
|
-
|
|
107
|
+
(see `client/examples/README.md` for how to read them).
|
|
108
|
+
|
|
109
|
+
| Script | Purpose |
|
|
110
|
+
|--------|---------|
|
|
111
|
+
| `run_all_examples.py` | Full API tour + runs all live sibling scripts |
|
|
112
|
+
| `ex_minimal_validated.py` | Smallest validated RPC example |
|
|
113
|
+
| `ex_universal_files.py` | All `UniversalFileClient` methods (open/edit/write/close/preview) |
|
|
114
|
+
| `ex_session_view_subordinates.py` | `session_view` and subordinate CRUD |
|
|
115
|
+
| `ex_file_sessions.py` | Sessions, locks, transfer roundtrip |
|
|
116
|
+
| `ex_config_only.py` | Parse `config.json` without TCP |
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
casmgr --config config.json start
|
|
120
|
+
python client/examples/run_all_examples.py
|
|
121
|
+
```
|
|
108
122
|
|
|
109
123
|
## Development
|
|
110
124
|
|
|
@@ -63,7 +63,7 @@ async with CodeAnalysisAsyncClient(host="127.0.0.1", port=15001) as client:
|
|
|
63
63
|
|
|
64
64
|
Use `call_unified_validated` when you need queue polling. Pass `refresh_schema=True` on a single call to bypass the in-memory schema cache.
|
|
65
65
|
|
|
66
|
-
## High-level facades (aligned with server
|
|
66
|
+
## High-level facades (aligned with live server registry)
|
|
67
67
|
|
|
68
68
|
The client does **not** wrap CST commands (`cst_load_file`, …) or legacy file I/O
|
|
69
69
|
(`universal_file_read`, `read_project_text_file`, …). Those commands are removed
|
|
@@ -71,28 +71,42 @@ from the server registry. Use the facades below or generic `call` / `commands.*`
|
|
|
71
71
|
|
|
72
72
|
| Facade | Property | Server commands |
|
|
73
73
|
|--------|----------|-----------------|
|
|
74
|
-
| Client DB sessions + transfer | `client.file_sessions` | `session_*`, `project_file_transfer_*`, `project_file_advisory_lock_batch` |
|
|
74
|
+
| Client DB sessions + transfer | `client.file_sessions` | `session_*`, `subordinate_session_*`, `project_file_transfer_*`, `project_file_advisory_lock_batch` |
|
|
75
75
|
| Universal edit sessions | `client.universal_files` | `universal_file_open`, `edit`, `write`, `close`, `preview` |
|
|
76
76
|
| Any registered command | `client.call` / `client.commands.<name>` | schema from live `help()` |
|
|
77
77
|
|
|
78
|
-
Canonical command lists: `code_analysis_client.server_api`
|
|
79
|
-
`
|
|
78
|
+
Canonical command lists: `code_analysis_client.server_api` — exported as
|
|
79
|
+
`FILE_SESSION_COMMANDS`, `FILE_SESSION_FACADE_METHODS`, `CLIENT_FACADE_COMMANDS`,
|
|
80
|
+
`REMOVED_COMMANDS`.
|
|
80
81
|
|
|
81
|
-
Sync
|
|
82
|
+
Sync checks (in-process registry):
|
|
82
83
|
|
|
83
84
|
```bash
|
|
84
|
-
pytest tests/test_client_server_api_sync.py
|
|
85
|
+
pytest tests/test_client_server_api_sync.py tests/test_code_analysis_client.py -k session
|
|
85
86
|
```
|
|
86
87
|
|
|
87
|
-
Package version
|
|
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
90
|
|
|
89
91
|
## Examples (this repository)
|
|
90
92
|
|
|
91
93
|
Runnable scripts live under `client/examples/`. **Long-form “man page” style
|
|
92
94
|
documentation** is embedded in the **module docstrings** of those Python files
|
|
93
|
-
(see `client/examples/README.md` for how to read them).
|
|
94
|
-
|
|
95
|
-
|
|
95
|
+
(see `client/examples/README.md` for how to read them).
|
|
96
|
+
|
|
97
|
+
| Script | Purpose |
|
|
98
|
+
|--------|---------|
|
|
99
|
+
| `run_all_examples.py` | Full API tour + runs all live sibling scripts |
|
|
100
|
+
| `ex_minimal_validated.py` | Smallest validated RPC example |
|
|
101
|
+
| `ex_universal_files.py` | All `UniversalFileClient` methods (open/edit/write/close/preview) |
|
|
102
|
+
| `ex_session_view_subordinates.py` | `session_view` and subordinate CRUD |
|
|
103
|
+
| `ex_file_sessions.py` | Sessions, locks, transfer roundtrip |
|
|
104
|
+
| `ex_config_only.py` | Parse `config.json` without TCP |
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
casmgr --config config.json start
|
|
108
|
+
python client/examples/run_all_examples.py
|
|
109
|
+
```
|
|
96
110
|
|
|
97
111
|
## Development
|
|
98
112
|
|
|
@@ -21,6 +21,8 @@ from code_analysis_client.file_session import FileSessionClient, SessionNotFound
|
|
|
21
21
|
from code_analysis_client.server_api import (
|
|
22
22
|
CLIENT_FACADE_COMMANDS,
|
|
23
23
|
CST_REMOVED_COMMANDS,
|
|
24
|
+
FILE_SESSION_COMMANDS,
|
|
25
|
+
FILE_SESSION_FACADE_METHODS,
|
|
24
26
|
LEGACY_REMOVED_COMMANDS,
|
|
25
27
|
REMOVED_COMMANDS,
|
|
26
28
|
UNIVERSAL_FILE_COMMANDS,
|
|
@@ -40,6 +42,8 @@ __all__ = [
|
|
|
40
42
|
"CST_REMOVED_COMMANDS",
|
|
41
43
|
"ClientValidationError",
|
|
42
44
|
"CodeAnalysisAsyncClient",
|
|
45
|
+
"FILE_SESSION_COMMANDS",
|
|
46
|
+
"FILE_SESSION_FACADE_METHODS",
|
|
43
47
|
"FileSessionClient",
|
|
44
48
|
"LEGACY_REMOVED_COMMANDS",
|
|
45
49
|
"REMOVED_COMMANDS",
|
|
@@ -123,7 +123,7 @@ class CodeAnalysisAsyncClient:
|
|
|
123
123
|
|
|
124
124
|
@property
|
|
125
125
|
def file_sessions(self) -> FileSessionClient:
|
|
126
|
-
"""Session
|
|
126
|
+
"""Session workflow: ``session_*``, ``subordinate_session_*``, transfer, advisory locks."""
|
|
127
127
|
return FileSessionClient(self)
|
|
128
128
|
|
|
129
129
|
@property
|
{code_analysis_client-1.0.5 → code_analysis_client-1.0.6}/code_analysis_client/file_session.py
RENAMED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
"""
|
|
2
2
|
High-level file transfer and session workflow on top of CodeAnalysisAsyncClient.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
lock/unlock indexed files without moving bytes.
|
|
4
|
+
Wraps ``session_*`` and ``subordinate_session_*`` MCP commands plus transfer and
|
|
5
|
+
advisory-lock commands that accept ``session_id``.
|
|
7
6
|
|
|
8
7
|
Author: Vasiliy Zdanovskiy
|
|
9
8
|
email: vasilyvz@gmail.com
|
|
@@ -32,6 +31,24 @@ def _unwrap(data: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
32
31
|
)
|
|
33
32
|
|
|
34
33
|
|
|
34
|
+
def _require_non_empty(value: str, *, field: str) -> str:
|
|
35
|
+
text = str(value or "").strip()
|
|
36
|
+
if not text:
|
|
37
|
+
raise ClientValidationError(f"{field} is required", field=field)
|
|
38
|
+
return text
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _validate_download_target(*, file_id: Optional[str]) -> None:
|
|
42
|
+
"""Download requires ``file_id``."""
|
|
43
|
+
fid = str(file_id or "").strip()
|
|
44
|
+
if not fid:
|
|
45
|
+
raise ClientValidationError("file_id is required", field="file_id")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _lock_mode_from_flag(lock: bool) -> str:
|
|
49
|
+
return "full" if lock else "none"
|
|
50
|
+
|
|
51
|
+
|
|
35
52
|
def _extract_file_id(payload: Dict[str, Any], *, field: str = "file_id") -> str:
|
|
36
53
|
"""Return non-empty ``file_id`` from a command payload or raise."""
|
|
37
54
|
fid = str(payload.get(field) or "").strip()
|
|
@@ -45,7 +62,7 @@ def _extract_file_id(payload: Dict[str, Any], *, field: str = "file_id") -> str:
|
|
|
45
62
|
|
|
46
63
|
|
|
47
64
|
class FileSessionClient:
|
|
48
|
-
"""Session-scoped file
|
|
65
|
+
"""Session-scoped DB sessions, subordinate links, file locks, and transfer helpers."""
|
|
49
66
|
|
|
50
67
|
__slots__ = ("_client",)
|
|
51
68
|
|
|
@@ -87,14 +104,163 @@ class FileSessionClient:
|
|
|
87
104
|
async def delete_session(
|
|
88
105
|
self, session_id: str, *, force: bool = False
|
|
89
106
|
) -> Dict[str, Any]:
|
|
90
|
-
"""Delete a client session (``session_delete``).
|
|
107
|
+
"""Delete a client session (``session_delete``).
|
|
108
|
+
|
|
109
|
+
``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``.
|
|
116
|
+
"""
|
|
117
|
+
params: Dict[str, Any] = {"session_id": session_id}
|
|
118
|
+
if force:
|
|
119
|
+
params["force"] = True
|
|
91
120
|
return _unwrap(
|
|
92
121
|
await self._client.call_validated(
|
|
93
122
|
"session_delete",
|
|
94
|
-
|
|
123
|
+
params,
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
async def view_session(self, session_id: str) -> Dict[str, Any]:
|
|
128
|
+
"""Aggregated session view (``session_view``).
|
|
129
|
+
|
|
130
|
+
Returns ``locked_files_by_project`` (file_id, file_path,
|
|
131
|
+
project_presentation per project) and ``subordinate_sessions``
|
|
132
|
+
(subordinate_session_id, server_uuid, session_presentation,
|
|
133
|
+
server_presentation, link_comment).
|
|
134
|
+
"""
|
|
135
|
+
sid = _require_non_empty(session_id, field="session_id")
|
|
136
|
+
return _unwrap(
|
|
137
|
+
await self._client.call_validated(
|
|
138
|
+
"session_view",
|
|
139
|
+
{"session_id": sid},
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
async def create_subordinate_session(
|
|
144
|
+
self,
|
|
145
|
+
parent_session_id: str,
|
|
146
|
+
subordinate_session_id: str,
|
|
147
|
+
comment: str,
|
|
148
|
+
*,
|
|
149
|
+
server_uuid: Optional[str] = None,
|
|
150
|
+
) -> Dict[str, Any]:
|
|
151
|
+
"""Link subordinate to parent (``subordinate_session_create``).
|
|
152
|
+
|
|
153
|
+
``server_uuid`` defaults to the server's ``registration.instance_uuid``
|
|
154
|
+
when omitted.
|
|
155
|
+
"""
|
|
156
|
+
params: Dict[str, Any] = {
|
|
157
|
+
"parent_session_id": _require_non_empty(
|
|
158
|
+
parent_session_id, field="parent_session_id"
|
|
159
|
+
),
|
|
160
|
+
"subordinate_session_id": _require_non_empty(
|
|
161
|
+
subordinate_session_id, field="subordinate_session_id"
|
|
162
|
+
),
|
|
163
|
+
"comment": comment,
|
|
164
|
+
}
|
|
165
|
+
if server_uuid is not None:
|
|
166
|
+
params["server_uuid"] = _require_non_empty(server_uuid, field="server_uuid")
|
|
167
|
+
return _unwrap(
|
|
168
|
+
await self._client.call_validated("subordinate_session_create", params)
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
async def get_subordinate_session(
|
|
172
|
+
self,
|
|
173
|
+
parent_session_id: str,
|
|
174
|
+
subordinate_session_id: str,
|
|
175
|
+
server_uuid: str,
|
|
176
|
+
) -> Dict[str, Any]:
|
|
177
|
+
"""Fetch one subordinate link (``subordinate_session_get``)."""
|
|
178
|
+
return _unwrap(
|
|
179
|
+
await self._client.call_validated(
|
|
180
|
+
"subordinate_session_get",
|
|
181
|
+
{
|
|
182
|
+
"parent_session_id": _require_non_empty(
|
|
183
|
+
parent_session_id, field="parent_session_id"
|
|
184
|
+
),
|
|
185
|
+
"subordinate_session_id": _require_non_empty(
|
|
186
|
+
subordinate_session_id, field="subordinate_session_id"
|
|
187
|
+
),
|
|
188
|
+
"server_uuid": _require_non_empty(server_uuid, field="server_uuid"),
|
|
189
|
+
},
|
|
95
190
|
)
|
|
96
191
|
)
|
|
97
192
|
|
|
193
|
+
async def update_subordinate_session(
|
|
194
|
+
self,
|
|
195
|
+
parent_session_id: str,
|
|
196
|
+
subordinate_session_id: str,
|
|
197
|
+
server_uuid: str,
|
|
198
|
+
comment: str,
|
|
199
|
+
) -> Dict[str, Any]:
|
|
200
|
+
"""Update link comment (``subordinate_session_update``)."""
|
|
201
|
+
return _unwrap(
|
|
202
|
+
await self._client.call_validated(
|
|
203
|
+
"subordinate_session_update",
|
|
204
|
+
{
|
|
205
|
+
"parent_session_id": _require_non_empty(
|
|
206
|
+
parent_session_id, field="parent_session_id"
|
|
207
|
+
),
|
|
208
|
+
"subordinate_session_id": _require_non_empty(
|
|
209
|
+
subordinate_session_id, field="subordinate_session_id"
|
|
210
|
+
),
|
|
211
|
+
"server_uuid": _require_non_empty(server_uuid, field="server_uuid"),
|
|
212
|
+
"comment": comment,
|
|
213
|
+
},
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
async def delete_subordinate_session(
|
|
218
|
+
self,
|
|
219
|
+
parent_session_id: str,
|
|
220
|
+
subordinate_session_id: str,
|
|
221
|
+
server_uuid: str,
|
|
222
|
+
) -> Dict[str, Any]:
|
|
223
|
+
"""Remove subordinate link only (``subordinate_session_delete``).
|
|
224
|
+
|
|
225
|
+
Does not delete ``client_sessions`` rows; use :meth:`delete_session` for that.
|
|
226
|
+
"""
|
|
227
|
+
return _unwrap(
|
|
228
|
+
await self._client.call_validated(
|
|
229
|
+
"subordinate_session_delete",
|
|
230
|
+
{
|
|
231
|
+
"parent_session_id": _require_non_empty(
|
|
232
|
+
parent_session_id, field="parent_session_id"
|
|
233
|
+
),
|
|
234
|
+
"subordinate_session_id": _require_non_empty(
|
|
235
|
+
subordinate_session_id, field="subordinate_session_id"
|
|
236
|
+
),
|
|
237
|
+
"server_uuid": _require_non_empty(server_uuid, field="server_uuid"),
|
|
238
|
+
},
|
|
239
|
+
)
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
async def list_subordinate_sessions(
|
|
243
|
+
self,
|
|
244
|
+
*,
|
|
245
|
+
parent_session_id: Optional[str] = None,
|
|
246
|
+
subordinate_session_id: Optional[str] = None,
|
|
247
|
+
server_uuid: Optional[str] = None,
|
|
248
|
+
) -> Dict[str, Any]:
|
|
249
|
+
"""List subordinate links (``subordinate_session_list``).
|
|
250
|
+
|
|
251
|
+
Returns ``links`` and ``count``. All filter parameters are optional.
|
|
252
|
+
"""
|
|
253
|
+
params: Dict[str, Any] = {}
|
|
254
|
+
if parent_session_id is not None:
|
|
255
|
+
params["parent_session_id"] = parent_session_id
|
|
256
|
+
if subordinate_session_id is not None:
|
|
257
|
+
params["subordinate_session_id"] = subordinate_session_id
|
|
258
|
+
if server_uuid is not None:
|
|
259
|
+
params["server_uuid"] = server_uuid
|
|
260
|
+
return _unwrap(
|
|
261
|
+
await self._client.call_validated("subordinate_session_list", params)
|
|
262
|
+
)
|
|
263
|
+
|
|
98
264
|
async def list_sessions(
|
|
99
265
|
self,
|
|
100
266
|
*,
|
|
@@ -217,32 +383,27 @@ class FileSessionClient:
|
|
|
217
383
|
)
|
|
218
384
|
)
|
|
219
385
|
|
|
220
|
-
async def
|
|
386
|
+
async def _begin_download(
|
|
221
387
|
self,
|
|
222
388
|
session_id: str,
|
|
223
389
|
*,
|
|
224
|
-
|
|
225
|
-
file_id: Optional[str] = None,
|
|
226
|
-
file_path: Optional[str] = None,
|
|
390
|
+
file_id: str,
|
|
227
391
|
compression: str = "identity",
|
|
228
|
-
|
|
392
|
+
lock: bool = True,
|
|
229
393
|
chunk_size: Optional[int] = None,
|
|
230
394
|
include_backup_history: bool = False,
|
|
231
395
|
) -> Dict[str, Any]:
|
|
232
396
|
"""Begin chunked download; returns payload including ``file_id`` and ``transfer_id``."""
|
|
397
|
+
fid = _require_non_empty(file_id, field="file_id")
|
|
398
|
+
_validate_download_target(file_id=fid)
|
|
233
399
|
await self.assert_session_exists(session_id)
|
|
234
400
|
params: Dict[str, Any] = {
|
|
235
401
|
"session_id": session_id,
|
|
236
402
|
"compression": compression,
|
|
237
|
-
"lock_mode":
|
|
403
|
+
"lock_mode": _lock_mode_from_flag(lock),
|
|
238
404
|
"include_backup_history": include_backup_history,
|
|
405
|
+
"file_id": fid,
|
|
239
406
|
}
|
|
240
|
-
if project_id is not None:
|
|
241
|
-
params["project_id"] = project_id
|
|
242
|
-
if file_id is not None:
|
|
243
|
-
params["file_id"] = file_id
|
|
244
|
-
if file_path is not None:
|
|
245
|
-
params["file_path"] = file_path
|
|
246
407
|
if chunk_size is not None:
|
|
247
408
|
params["chunk_size"] = chunk_size
|
|
248
409
|
payload = _unwrap(
|
|
@@ -254,36 +415,24 @@ class FileSessionClient:
|
|
|
254
415
|
_extract_file_id(payload)
|
|
255
416
|
return payload
|
|
256
417
|
|
|
257
|
-
async def
|
|
258
|
-
self,
|
|
259
|
-
transfer_id: str,
|
|
260
|
-
destination: Union[str, Path],
|
|
261
|
-
) -> Any:
|
|
262
|
-
"""Stream download chunks to ``destination`` via the adapter client."""
|
|
263
|
-
return await self._client.rpc.download_file(
|
|
264
|
-
str(transfer_id).strip(),
|
|
265
|
-
str(destination),
|
|
266
|
-
)
|
|
267
|
-
|
|
268
|
-
async def download_file_locked(
|
|
418
|
+
async def download(
|
|
269
419
|
self,
|
|
270
420
|
session_id: str,
|
|
271
421
|
destination: Union[str, Path],
|
|
422
|
+
file_id: str,
|
|
272
423
|
*,
|
|
273
|
-
project_id: Optional[str] = None,
|
|
274
|
-
file_id: Optional[str] = None,
|
|
275
|
-
file_path: Optional[str] = None,
|
|
276
424
|
compression: str = "identity",
|
|
277
|
-
|
|
425
|
+
lock: bool = True,
|
|
426
|
+
include_backup_history: bool = False,
|
|
278
427
|
) -> Tuple[Dict[str, Any], Any]:
|
|
279
|
-
"""Download
|
|
280
|
-
|
|
428
|
+
"""Download an indexed file by ``file_id``; returns (begin_payload, receipt)."""
|
|
429
|
+
fid = _require_non_empty(file_id, field="file_id")
|
|
430
|
+
begin = await self._begin_download(
|
|
281
431
|
session_id,
|
|
282
|
-
|
|
283
|
-
file_id=file_id,
|
|
284
|
-
file_path=file_path,
|
|
432
|
+
file_id=fid,
|
|
285
433
|
compression=compression,
|
|
286
|
-
|
|
434
|
+
lock=lock,
|
|
435
|
+
include_backup_history=include_backup_history,
|
|
287
436
|
)
|
|
288
437
|
transfer_id = str(begin.get("transfer_id") or "").strip()
|
|
289
438
|
if not transfer_id:
|
|
@@ -295,6 +444,17 @@ class FileSessionClient:
|
|
|
295
444
|
receipt = await self.download_to_path(transfer_id, destination)
|
|
296
445
|
return begin, receipt
|
|
297
446
|
|
|
447
|
+
async def download_to_path(
|
|
448
|
+
self,
|
|
449
|
+
transfer_id: str,
|
|
450
|
+
destination: Union[str, Path],
|
|
451
|
+
) -> Any:
|
|
452
|
+
"""Stream download chunks to ``destination`` via the adapter client."""
|
|
453
|
+
return await self._client.rpc.download_file(
|
|
454
|
+
str(transfer_id).strip(),
|
|
455
|
+
str(destination),
|
|
456
|
+
)
|
|
457
|
+
|
|
298
458
|
async def upload_bytes(
|
|
299
459
|
self,
|
|
300
460
|
payload: bytes,
|
|
@@ -315,7 +475,7 @@ class FileSessionClient:
|
|
|
315
475
|
if dest.is_file():
|
|
316
476
|
dest.unlink(missing_ok=True)
|
|
317
477
|
|
|
318
|
-
async def
|
|
478
|
+
async def _commit_upload(
|
|
319
479
|
self,
|
|
320
480
|
session_id: str,
|
|
321
481
|
transfer_id: str,
|
|
@@ -327,7 +487,7 @@ class FileSessionClient:
|
|
|
327
487
|
dry_run: bool = False,
|
|
328
488
|
backup: bool = True,
|
|
329
489
|
) -> Dict[str, Any]:
|
|
330
|
-
"""Persist
|
|
490
|
+
"""Persist a completed adapter upload (``project_file_transfer_upload_save``)."""
|
|
331
491
|
await self.assert_session_exists(session_id)
|
|
332
492
|
params: Dict[str, Any] = {
|
|
333
493
|
"session_id": session_id,
|
|
@@ -342,31 +502,78 @@ class FileSessionClient:
|
|
|
342
502
|
params["file_id"] = file_id
|
|
343
503
|
if file_path is not None:
|
|
344
504
|
params["file_path"] = file_path
|
|
345
|
-
|
|
505
|
+
return _unwrap(
|
|
346
506
|
await self._client.call_validated(
|
|
347
507
|
"project_file_transfer_upload_save",
|
|
348
508
|
params,
|
|
349
509
|
)
|
|
350
510
|
)
|
|
351
|
-
if not dry_run:
|
|
352
|
-
_extract_file_id(payload)
|
|
353
|
-
return payload
|
|
354
511
|
|
|
355
|
-
async def
|
|
512
|
+
async def upload_new(
|
|
356
513
|
self,
|
|
357
514
|
session_id: str,
|
|
358
515
|
payload: bytes,
|
|
516
|
+
project_id: str,
|
|
517
|
+
file_path: str,
|
|
359
518
|
*,
|
|
360
|
-
project_id: Optional[str] = None,
|
|
361
|
-
file_id: Optional[str] = None,
|
|
362
|
-
file_path: Optional[str] = None,
|
|
363
519
|
filename: Optional[str] = None,
|
|
364
520
|
compression: str = "identity",
|
|
365
|
-
|
|
521
|
+
unlock: bool = True,
|
|
522
|
+
dry_run: bool = False,
|
|
366
523
|
backup: bool = True,
|
|
524
|
+
) -> str:
|
|
525
|
+
"""Create a new project file from uploaded bytes; returns the new ``file_id``.
|
|
526
|
+
|
|
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.
|
|
529
|
+
"""
|
|
530
|
+
pid = _require_non_empty(project_id, field="project_id")
|
|
531
|
+
fp = _require_non_empty(file_path, field="file_path")
|
|
532
|
+
name = filename or Path(fp).name
|
|
533
|
+
receipt = await self.upload_bytes(
|
|
534
|
+
payload,
|
|
535
|
+
filename=name,
|
|
536
|
+
compression=compression,
|
|
537
|
+
)
|
|
538
|
+
if not getattr(receipt, "completed", False):
|
|
539
|
+
raise ClientValidationError(
|
|
540
|
+
"upload did not complete",
|
|
541
|
+
field="transfer_id",
|
|
542
|
+
details={"receipt": repr(receipt)},
|
|
543
|
+
)
|
|
544
|
+
saved = await self._commit_upload(
|
|
545
|
+
session_id,
|
|
546
|
+
str(receipt.transfer_id),
|
|
547
|
+
project_id=pid,
|
|
548
|
+
file_path=fp,
|
|
549
|
+
unlock_after_write=unlock,
|
|
550
|
+
backup=backup,
|
|
551
|
+
dry_run=dry_run,
|
|
552
|
+
)
|
|
553
|
+
if dry_run:
|
|
554
|
+
fid = str(saved.get("file_id") or "").strip()
|
|
555
|
+
return fid
|
|
556
|
+
return _extract_file_id(saved)
|
|
557
|
+
|
|
558
|
+
async def upload(
|
|
559
|
+
self,
|
|
560
|
+
session_id: str,
|
|
561
|
+
payload: bytes,
|
|
562
|
+
file_id: str,
|
|
563
|
+
*,
|
|
564
|
+
filename: Optional[str] = None,
|
|
565
|
+
compression: str = "identity",
|
|
566
|
+
unlock: bool = True,
|
|
567
|
+
backup: bool = True,
|
|
568
|
+
dry_run: bool = False,
|
|
367
569
|
) -> Dict[str, Any]:
|
|
368
|
-
"""
|
|
369
|
-
|
|
570
|
+
"""Overwrite an existing indexed file from uploaded bytes.
|
|
571
|
+
|
|
572
|
+
Only ``file_id`` is required. Returns the server save payload on success
|
|
573
|
+
or raises :class:`ClientValidationError` on failure.
|
|
574
|
+
"""
|
|
575
|
+
fid = _require_non_empty(file_id, field="file_id")
|
|
576
|
+
name = filename or "payload.bin"
|
|
370
577
|
receipt = await self.upload_bytes(
|
|
371
578
|
payload,
|
|
372
579
|
filename=name,
|
|
@@ -378,12 +585,14 @@ class FileSessionClient:
|
|
|
378
585
|
field="transfer_id",
|
|
379
586
|
details={"receipt": repr(receipt)},
|
|
380
587
|
)
|
|
381
|
-
|
|
588
|
+
saved = await self._commit_upload(
|
|
382
589
|
session_id,
|
|
383
590
|
str(receipt.transfer_id),
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
file_path=file_path,
|
|
387
|
-
unlock_after_write=unlock_after_write,
|
|
591
|
+
file_id=fid,
|
|
592
|
+
unlock_after_write=unlock,
|
|
388
593
|
backup=backup,
|
|
594
|
+
dry_run=dry_run,
|
|
389
595
|
)
|
|
596
|
+
if not dry_run:
|
|
597
|
+
_extract_file_id(saved)
|
|
598
|
+
return saved
|
{code_analysis_client-1.0.5 → code_analysis_client-1.0.6}/code_analysis_client/server_api.py
RENAMED
|
@@ -12,7 +12,7 @@ email: vasilyvz@gmail.com
|
|
|
12
12
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
|
-
from typing import Callable, FrozenSet
|
|
15
|
+
from typing import Callable, Dict, FrozenSet
|
|
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(
|
|
@@ -54,9 +54,15 @@ FILE_SESSION_COMMANDS: FrozenSet[str] = frozenset(
|
|
|
54
54
|
"session_create",
|
|
55
55
|
"session_delete",
|
|
56
56
|
"session_list",
|
|
57
|
+
"session_view",
|
|
57
58
|
"session_open_file",
|
|
58
59
|
"session_close_file",
|
|
59
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",
|
|
60
66
|
}
|
|
61
67
|
)
|
|
62
68
|
|
|
@@ -82,6 +88,41 @@ CLIENT_FACADE_COMMANDS: FrozenSet[str] = (
|
|
|
82
88
|
FILE_SESSION_COMMANDS | TRANSFER_AND_LOCK_COMMANDS | UNIVERSAL_FILE_COMMANDS
|
|
83
89
|
)
|
|
84
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
|
+
|
|
85
126
|
|
|
86
127
|
def _command_registered(get_command: Callable[[str], object], name: str) -> bool:
|
|
87
128
|
try:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.0.6
|
{code_analysis_client-1.0.5 → code_analysis_client-1.0.6}/code_analysis_client.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-analysis-client
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.6
|
|
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
|
|
@@ -75,7 +75,7 @@ async with CodeAnalysisAsyncClient(host="127.0.0.1", port=15001) as client:
|
|
|
75
75
|
|
|
76
76
|
Use `call_unified_validated` when you need queue polling. Pass `refresh_schema=True` on a single call to bypass the in-memory schema cache.
|
|
77
77
|
|
|
78
|
-
## High-level facades (aligned with server
|
|
78
|
+
## High-level facades (aligned with live server registry)
|
|
79
79
|
|
|
80
80
|
The client does **not** wrap CST commands (`cst_load_file`, …) or legacy file I/O
|
|
81
81
|
(`universal_file_read`, `read_project_text_file`, …). Those commands are removed
|
|
@@ -83,28 +83,42 @@ from the server registry. Use the facades below or generic `call` / `commands.*`
|
|
|
83
83
|
|
|
84
84
|
| Facade | Property | Server commands |
|
|
85
85
|
|--------|----------|-----------------|
|
|
86
|
-
| Client DB sessions + transfer | `client.file_sessions` | `session_*`, `project_file_transfer_*`, `project_file_advisory_lock_batch` |
|
|
86
|
+
| Client DB sessions + transfer | `client.file_sessions` | `session_*`, `subordinate_session_*`, `project_file_transfer_*`, `project_file_advisory_lock_batch` |
|
|
87
87
|
| Universal edit sessions | `client.universal_files` | `universal_file_open`, `edit`, `write`, `close`, `preview` |
|
|
88
88
|
| Any registered command | `client.call` / `client.commands.<name>` | schema from live `help()` |
|
|
89
89
|
|
|
90
|
-
Canonical command lists: `code_analysis_client.server_api`
|
|
91
|
-
`
|
|
90
|
+
Canonical command lists: `code_analysis_client.server_api` — exported as
|
|
91
|
+
`FILE_SESSION_COMMANDS`, `FILE_SESSION_FACADE_METHODS`, `CLIENT_FACADE_COMMANDS`,
|
|
92
|
+
`REMOVED_COMMANDS`.
|
|
92
93
|
|
|
93
|
-
Sync
|
|
94
|
+
Sync checks (in-process registry):
|
|
94
95
|
|
|
95
96
|
```bash
|
|
96
|
-
pytest tests/test_client_server_api_sync.py
|
|
97
|
+
pytest tests/test_client_server_api_sync.py tests/test_code_analysis_client.py -k session
|
|
97
98
|
```
|
|
98
99
|
|
|
99
|
-
Package version
|
|
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
102
|
|
|
101
103
|
## Examples (this repository)
|
|
102
104
|
|
|
103
105
|
Runnable scripts live under `client/examples/`. **Long-form “man page” style
|
|
104
106
|
documentation** is embedded in the **module docstrings** of those Python files
|
|
105
|
-
(see `client/examples/README.md` for how to read them).
|
|
106
|
-
|
|
107
|
-
|
|
107
|
+
(see `client/examples/README.md` for how to read them).
|
|
108
|
+
|
|
109
|
+
| Script | Purpose |
|
|
110
|
+
|--------|---------|
|
|
111
|
+
| `run_all_examples.py` | Full API tour + runs all live sibling scripts |
|
|
112
|
+
| `ex_minimal_validated.py` | Smallest validated RPC example |
|
|
113
|
+
| `ex_universal_files.py` | All `UniversalFileClient` methods (open/edit/write/close/preview) |
|
|
114
|
+
| `ex_session_view_subordinates.py` | `session_view` and subordinate CRUD |
|
|
115
|
+
| `ex_file_sessions.py` | Sessions, locks, transfer roundtrip |
|
|
116
|
+
| `ex_config_only.py` | Parse `config.json` without TCP |
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
casmgr --config config.json start
|
|
120
|
+
python client/examples/run_all_examples.py
|
|
121
|
+
```
|
|
108
122
|
|
|
109
123
|
## Development
|
|
110
124
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
1.0.5
|
{code_analysis_client-1.0.5 → code_analysis_client-1.0.6}/code_analysis_client/commands_proxy.py
RENAMED
|
File without changes
|
|
File without changes
|
{code_analysis_client-1.0.5 → code_analysis_client-1.0.6}/code_analysis_client/exceptions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{code_analysis_client-1.0.5 → code_analysis_client-1.0.6}/code_analysis_client/server_schema.py
RENAMED
|
File without changes
|
{code_analysis_client-1.0.5 → code_analysis_client-1.0.6}/code_analysis_client/universal_file.py
RENAMED
|
File without changes
|
{code_analysis_client-1.0.5 → code_analysis_client-1.0.6}/code_analysis_client/validation.py
RENAMED
|
File without changes
|
{code_analysis_client-1.0.5 → code_analysis_client-1.0.6}/code_analysis_client.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{code_analysis_client-1.0.5 → code_analysis_client-1.0.6}/code_analysis_client.egg-info/requires.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|