code-analysis-client 1.0.4__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.4 → code_analysis_client-1.0.6}/PKG-INFO +25 -11
- {code_analysis_client-1.0.4 → code_analysis_client-1.0.6}/README.md +24 -10
- {code_analysis_client-1.0.4 → code_analysis_client-1.0.6}/code_analysis_client/__init__.py +4 -0
- {code_analysis_client-1.0.4 → code_analysis_client-1.0.6}/code_analysis_client/client.py +1 -1
- {code_analysis_client-1.0.4 → code_analysis_client-1.0.6}/code_analysis_client/file_session.py +282 -56
- {code_analysis_client-1.0.4 → 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.4 → code_analysis_client-1.0.6}/code_analysis_client.egg-info/PKG-INFO +25 -11
- code_analysis_client-1.0.4/code_analysis_client/version.txt +0 -1
- {code_analysis_client-1.0.4 → code_analysis_client-1.0.6}/code_analysis_client/commands_proxy.py +0 -0
- {code_analysis_client-1.0.4 → code_analysis_client-1.0.6}/code_analysis_client/config.py +0 -0
- {code_analysis_client-1.0.4 → code_analysis_client-1.0.6}/code_analysis_client/exceptions.py +0 -0
- {code_analysis_client-1.0.4 → code_analysis_client-1.0.6}/code_analysis_client/py.typed +0 -0
- {code_analysis_client-1.0.4 → code_analysis_client-1.0.6}/code_analysis_client/responses.py +0 -0
- {code_analysis_client-1.0.4 → code_analysis_client-1.0.6}/code_analysis_client/server_schema.py +0 -0
- {code_analysis_client-1.0.4 → code_analysis_client-1.0.6}/code_analysis_client/universal_file.py +0 -0
- {code_analysis_client-1.0.4 → code_analysis_client-1.0.6}/code_analysis_client/validation.py +0 -0
- {code_analysis_client-1.0.4 → code_analysis_client-1.0.6}/code_analysis_client.egg-info/SOURCES.txt +0 -0
- {code_analysis_client-1.0.4 → code_analysis_client-1.0.6}/code_analysis_client.egg-info/dependency_links.txt +0 -0
- {code_analysis_client-1.0.4 → code_analysis_client-1.0.6}/code_analysis_client.egg-info/requires.txt +0 -0
- {code_analysis_client-1.0.4 → code_analysis_client-1.0.6}/code_analysis_client.egg-info/top_level.txt +0 -0
- {code_analysis_client-1.0.4 → code_analysis_client-1.0.6}/pyproject.toml +0 -0
- {code_analysis_client-1.0.4 → 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.4 → 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,8 +31,38 @@ 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
|
+
|
|
52
|
+
def _extract_file_id(payload: Dict[str, Any], *, field: str = "file_id") -> str:
|
|
53
|
+
"""Return non-empty ``file_id`` from a command payload or raise."""
|
|
54
|
+
fid = str(payload.get(field) or "").strip()
|
|
55
|
+
if not fid:
|
|
56
|
+
raise ClientValidationError(
|
|
57
|
+
f"{field} missing from server response",
|
|
58
|
+
field=field,
|
|
59
|
+
details=payload,
|
|
60
|
+
)
|
|
61
|
+
return fid
|
|
62
|
+
|
|
63
|
+
|
|
35
64
|
class FileSessionClient:
|
|
36
|
-
"""Session-scoped file
|
|
65
|
+
"""Session-scoped DB sessions, subordinate links, file locks, and transfer helpers."""
|
|
37
66
|
|
|
38
67
|
__slots__ = ("_client",)
|
|
39
68
|
|
|
@@ -75,14 +104,163 @@ class FileSessionClient:
|
|
|
75
104
|
async def delete_session(
|
|
76
105
|
self, session_id: str, *, force: bool = False
|
|
77
106
|
) -> Dict[str, Any]:
|
|
78
|
-
"""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
|
|
79
120
|
return _unwrap(
|
|
80
121
|
await self._client.call_validated(
|
|
81
122
|
"session_delete",
|
|
82
|
-
|
|
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
|
+
},
|
|
190
|
+
)
|
|
191
|
+
)
|
|
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
|
+
},
|
|
83
214
|
)
|
|
84
215
|
)
|
|
85
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
|
+
|
|
86
264
|
async def list_sessions(
|
|
87
265
|
self,
|
|
88
266
|
*,
|
|
@@ -205,71 +383,56 @@ class FileSessionClient:
|
|
|
205
383
|
)
|
|
206
384
|
)
|
|
207
385
|
|
|
208
|
-
async def
|
|
386
|
+
async def _begin_download(
|
|
209
387
|
self,
|
|
210
388
|
session_id: str,
|
|
211
389
|
*,
|
|
212
|
-
|
|
213
|
-
file_id: Optional[str] = None,
|
|
214
|
-
file_path: Optional[str] = None,
|
|
390
|
+
file_id: str,
|
|
215
391
|
compression: str = "identity",
|
|
216
|
-
|
|
392
|
+
lock: bool = True,
|
|
217
393
|
chunk_size: Optional[int] = None,
|
|
218
394
|
include_backup_history: bool = False,
|
|
219
395
|
) -> Dict[str, Any]:
|
|
220
|
-
"""
|
|
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)
|
|
221
399
|
await self.assert_session_exists(session_id)
|
|
222
400
|
params: Dict[str, Any] = {
|
|
223
401
|
"session_id": session_id,
|
|
224
402
|
"compression": compression,
|
|
225
|
-
"lock_mode":
|
|
403
|
+
"lock_mode": _lock_mode_from_flag(lock),
|
|
226
404
|
"include_backup_history": include_backup_history,
|
|
405
|
+
"file_id": fid,
|
|
227
406
|
}
|
|
228
|
-
if project_id is not None:
|
|
229
|
-
params["project_id"] = project_id
|
|
230
|
-
if file_id is not None:
|
|
231
|
-
params["file_id"] = file_id
|
|
232
|
-
if file_path is not None:
|
|
233
|
-
params["file_path"] = file_path
|
|
234
407
|
if chunk_size is not None:
|
|
235
408
|
params["chunk_size"] = chunk_size
|
|
236
|
-
|
|
409
|
+
payload = _unwrap(
|
|
237
410
|
await self._client.call_validated(
|
|
238
411
|
"project_file_transfer_download_begin",
|
|
239
412
|
params,
|
|
240
413
|
)
|
|
241
414
|
)
|
|
415
|
+
_extract_file_id(payload)
|
|
416
|
+
return payload
|
|
242
417
|
|
|
243
|
-
async def
|
|
244
|
-
self,
|
|
245
|
-
transfer_id: str,
|
|
246
|
-
destination: Union[str, Path],
|
|
247
|
-
) -> Any:
|
|
248
|
-
"""Stream download chunks to ``destination`` via the adapter client."""
|
|
249
|
-
return await self._client.rpc.download_file(
|
|
250
|
-
str(transfer_id).strip(),
|
|
251
|
-
str(destination),
|
|
252
|
-
)
|
|
253
|
-
|
|
254
|
-
async def download_file_locked(
|
|
418
|
+
async def download(
|
|
255
419
|
self,
|
|
256
420
|
session_id: str,
|
|
257
421
|
destination: Union[str, Path],
|
|
422
|
+
file_id: str,
|
|
258
423
|
*,
|
|
259
|
-
project_id: Optional[str] = None,
|
|
260
|
-
file_id: Optional[str] = None,
|
|
261
|
-
file_path: Optional[str] = None,
|
|
262
424
|
compression: str = "identity",
|
|
263
|
-
|
|
425
|
+
lock: bool = True,
|
|
426
|
+
include_backup_history: bool = False,
|
|
264
427
|
) -> Tuple[Dict[str, Any], Any]:
|
|
265
|
-
"""Download
|
|
266
|
-
|
|
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(
|
|
267
431
|
session_id,
|
|
268
|
-
|
|
269
|
-
file_id=file_id,
|
|
270
|
-
file_path=file_path,
|
|
432
|
+
file_id=fid,
|
|
271
433
|
compression=compression,
|
|
272
|
-
|
|
434
|
+
lock=lock,
|
|
435
|
+
include_backup_history=include_backup_history,
|
|
273
436
|
)
|
|
274
437
|
transfer_id = str(begin.get("transfer_id") or "").strip()
|
|
275
438
|
if not transfer_id:
|
|
@@ -281,6 +444,17 @@ class FileSessionClient:
|
|
|
281
444
|
receipt = await self.download_to_path(transfer_id, destination)
|
|
282
445
|
return begin, receipt
|
|
283
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
|
+
|
|
284
458
|
async def upload_bytes(
|
|
285
459
|
self,
|
|
286
460
|
payload: bytes,
|
|
@@ -301,7 +475,7 @@ class FileSessionClient:
|
|
|
301
475
|
if dest.is_file():
|
|
302
476
|
dest.unlink(missing_ok=True)
|
|
303
477
|
|
|
304
|
-
async def
|
|
478
|
+
async def _commit_upload(
|
|
305
479
|
self,
|
|
306
480
|
session_id: str,
|
|
307
481
|
transfer_id: str,
|
|
@@ -313,7 +487,7 @@ class FileSessionClient:
|
|
|
313
487
|
dry_run: bool = False,
|
|
314
488
|
backup: bool = True,
|
|
315
489
|
) -> Dict[str, Any]:
|
|
316
|
-
"""Persist
|
|
490
|
+
"""Persist a completed adapter upload (``project_file_transfer_upload_save``)."""
|
|
317
491
|
await self.assert_session_exists(session_id)
|
|
318
492
|
params: Dict[str, Any] = {
|
|
319
493
|
"session_id": session_id,
|
|
@@ -335,21 +509,71 @@ class FileSessionClient:
|
|
|
335
509
|
)
|
|
336
510
|
)
|
|
337
511
|
|
|
338
|
-
async def
|
|
512
|
+
async def upload_new(
|
|
339
513
|
self,
|
|
340
514
|
session_id: str,
|
|
341
515
|
payload: bytes,
|
|
516
|
+
project_id: str,
|
|
517
|
+
file_path: str,
|
|
342
518
|
*,
|
|
343
|
-
project_id: Optional[str] = None,
|
|
344
|
-
file_id: Optional[str] = None,
|
|
345
|
-
file_path: Optional[str] = None,
|
|
346
519
|
filename: Optional[str] = None,
|
|
347
520
|
compression: str = "identity",
|
|
348
|
-
|
|
521
|
+
unlock: bool = True,
|
|
522
|
+
dry_run: bool = False,
|
|
349
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,
|
|
350
569
|
) -> Dict[str, Any]:
|
|
351
|
-
"""
|
|
352
|
-
|
|
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"
|
|
353
577
|
receipt = await self.upload_bytes(
|
|
354
578
|
payload,
|
|
355
579
|
filename=name,
|
|
@@ -361,12 +585,14 @@ class FileSessionClient:
|
|
|
361
585
|
field="transfer_id",
|
|
362
586
|
details={"receipt": repr(receipt)},
|
|
363
587
|
)
|
|
364
|
-
|
|
588
|
+
saved = await self._commit_upload(
|
|
365
589
|
session_id,
|
|
366
590
|
str(receipt.transfer_id),
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
file_path=file_path,
|
|
370
|
-
unlock_after_write=unlock_after_write,
|
|
591
|
+
file_id=fid,
|
|
592
|
+
unlock_after_write=unlock,
|
|
371
593
|
backup=backup,
|
|
594
|
+
dry_run=dry_run,
|
|
372
595
|
)
|
|
596
|
+
if not dry_run:
|
|
597
|
+
_extract_file_id(saved)
|
|
598
|
+
return saved
|
{code_analysis_client-1.0.4 → 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.4 → 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.4
|
{code_analysis_client-1.0.4 → code_analysis_client-1.0.6}/code_analysis_client/commands_proxy.py
RENAMED
|
File without changes
|
|
File without changes
|
{code_analysis_client-1.0.4 → 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.4 → code_analysis_client-1.0.6}/code_analysis_client/server_schema.py
RENAMED
|
File without changes
|
{code_analysis_client-1.0.4 → code_analysis_client-1.0.6}/code_analysis_client/universal_file.py
RENAMED
|
File without changes
|
{code_analysis_client-1.0.4 → code_analysis_client-1.0.6}/code_analysis_client/validation.py
RENAMED
|
File without changes
|
{code_analysis_client-1.0.4 → 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.4 → 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
|