code-analysis-client-dev 1.6.341__py3-none-any.whl
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/__init__.py +142 -0
- code_analysis_client/check_chain.py +374 -0
- code_analysis_client/client.py +728 -0
- code_analysis_client/commands_proxy.py +63 -0
- code_analysis_client/config.py +198 -0
- code_analysis_client/exceptions.py +188 -0
- code_analysis_client/file_session.py +823 -0
- code_analysis_client/project.py +59 -0
- code_analysis_client/py.typed +0 -0
- code_analysis_client/queue_wait.py +341 -0
- code_analysis_client/responses.py +126 -0
- code_analysis_client/schema_disk_cache.py +207 -0
- code_analysis_client/server_api.py +291 -0
- code_analysis_client/server_schema.py +57 -0
- code_analysis_client/symbol_tree.py +13 -0
- code_analysis_client/universal_file.py +50 -0
- code_analysis_client/validation.py +98 -0
- code_analysis_client/vectorizer_state.py +159 -0
- code_analysis_client/version.txt +1 -0
- code_analysis_client/worktree.py +533 -0
- code_analysis_client_dev-1.6.341.dist-info/METADATA +251 -0
- code_analysis_client_dev-1.6.341.dist-info/RECORD +24 -0
- code_analysis_client_dev-1.6.341.dist-info/WHEEL +5 -0
- code_analysis_client_dev-1.6.341.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cross-process on-disk cache for per-command JSON schemas (bug 8e6acb34, Fix 1).
|
|
3
|
+
|
|
4
|
+
Root cause this addresses: :meth:`CodeAnalysisAsyncClient.get_command_schema`
|
|
5
|
+
already memoizes a schema for the lifetime of one client instance
|
|
6
|
+
(``self._command_schema_cache``), but that in-memory cache starts empty on
|
|
7
|
+
every new process. Most real callers (one-shot CLI invocations, the
|
|
8
|
+
``pipeline`` live checks, individual agent tool calls) construct a fresh
|
|
9
|
+
:class:`~code_analysis_client.client.CodeAnalysisAsyncClient`, call a command
|
|
10
|
+
once, and exit -- so the in-memory cache never pays for itself and every
|
|
11
|
+
single ``call_validated``/``commands.<name>()`` invocation pays a full extra
|
|
12
|
+
``help(cmdname)`` network round trip before the actual command call.
|
|
13
|
+
|
|
14
|
+
Measured on the deployed server (192.168.254.26:15010), warm TCP connection,
|
|
15
|
+
first ``call_validated("health", {})`` on a fresh client instance (schema
|
|
16
|
+
cache miss) vs the same call with schema already cached in memory: the
|
|
17
|
+
``help`` round trip costs ~2.7ms median -- matching bug 8e6acb34's reported
|
|
18
|
+
~3.0ms "our own wrapper" figure almost exactly. This module removes that
|
|
19
|
+
network round trip on repeat runs by persisting each fetched schema to a
|
|
20
|
+
small JSON file on local disk, keyed by (server base URL, command name),
|
|
21
|
+
with a bounded TTL so a stale schema is never trusted for long after a
|
|
22
|
+
server-side deploy changes it.
|
|
23
|
+
|
|
24
|
+
This does NOT weaken validation: the exact same schema shape is used either
|
|
25
|
+
way, just fetched from local disk instead of the network when it is still
|
|
26
|
+
fresh. ``refresh=True`` (existing :meth:`get_command_schema` parameter) and
|
|
27
|
+
:meth:`CodeAnalysisAsyncClient.clear_command_schema_cache` (which now also
|
|
28
|
+
purges the on-disk entries for that server) remain the explicit invalidation
|
|
29
|
+
paths for when a caller knows the server's command schemas changed (e.g.
|
|
30
|
+
after ``reload``).
|
|
31
|
+
|
|
32
|
+
Configuration (env vars, all optional):
|
|
33
|
+
``CODE_ANALYSIS_CLIENT_SCHEMA_CACHE_DIR`` -- override the cache root
|
|
34
|
+
(default: ``~/.cache/code_analysis_client/schema``).
|
|
35
|
+
``CODE_ANALYSIS_CLIENT_SCHEMA_CACHE_TTL_SECONDS`` -- override the
|
|
36
|
+
freshness window (default: 300 seconds -- long enough to amortize
|
|
37
|
+
the round trip across a short-lived process's calls and across
|
|
38
|
+
back-to-back process invocations, short enough that a schema change
|
|
39
|
+
shipped by a deploy is picked up within five minutes without any
|
|
40
|
+
explicit action).
|
|
41
|
+
``CODE_ANALYSIS_CLIENT_DISABLE_SCHEMA_CACHE`` -- set to ``1``/``true`` to
|
|
42
|
+
disable on-disk caching entirely and fall back to the original
|
|
43
|
+
per-instance-only, always-network behavior.
|
|
44
|
+
|
|
45
|
+
Every function here is best-effort: any I/O or (de)serialization failure is
|
|
46
|
+
swallowed and treated as a cache miss / no-op. A broken or unwritable cache
|
|
47
|
+
directory must never break a command call, only forfeit the speedup.
|
|
48
|
+
|
|
49
|
+
Author: Vasiliy Zdanovskiy
|
|
50
|
+
email: vasilyvz@gmail.com
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
from __future__ import annotations
|
|
54
|
+
|
|
55
|
+
import hashlib
|
|
56
|
+
import json
|
|
57
|
+
import os
|
|
58
|
+
import time
|
|
59
|
+
from pathlib import Path
|
|
60
|
+
from typing import Any, Dict, Optional
|
|
61
|
+
|
|
62
|
+
_ENV_CACHE_DIR = "CODE_ANALYSIS_CLIENT_SCHEMA_CACHE_DIR"
|
|
63
|
+
_ENV_TTL_SECONDS = "CODE_ANALYSIS_CLIENT_SCHEMA_CACHE_TTL_SECONDS"
|
|
64
|
+
_ENV_DISABLE = "CODE_ANALYSIS_CLIENT_DISABLE_SCHEMA_CACHE"
|
|
65
|
+
|
|
66
|
+
_DEFAULT_TTL_SECONDS = 300.0
|
|
67
|
+
_TRUTHY = {"1", "true", "yes", "on"}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _disabled() -> bool:
|
|
71
|
+
"""Return True when on-disk schema caching is disabled via env var."""
|
|
72
|
+
return os.environ.get(_ENV_DISABLE, "").strip().lower() in _TRUTHY
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _cache_root() -> Optional[Path]:
|
|
76
|
+
"""Return the configured cache root directory, or None when disabled."""
|
|
77
|
+
if _disabled():
|
|
78
|
+
return None
|
|
79
|
+
override = os.environ.get(_ENV_CACHE_DIR)
|
|
80
|
+
if override:
|
|
81
|
+
return Path(override)
|
|
82
|
+
return Path.home() / ".cache" / "code_analysis_client" / "schema"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _ttl_seconds() -> float:
|
|
86
|
+
"""Return the configured freshness window in seconds."""
|
|
87
|
+
raw = os.environ.get(_ENV_TTL_SECONDS)
|
|
88
|
+
if raw:
|
|
89
|
+
try:
|
|
90
|
+
return float(raw)
|
|
91
|
+
except ValueError:
|
|
92
|
+
pass
|
|
93
|
+
return _DEFAULT_TTL_SECONDS
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _server_dir(root: Path, base_url: str) -> Path:
|
|
97
|
+
"""Return the per-server subdirectory for ``base_url`` under ``root``."""
|
|
98
|
+
server_key = hashlib.sha256(base_url.encode("utf-8")).hexdigest()[:16]
|
|
99
|
+
return root / server_key
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _safe_command_filename(command: str) -> str:
|
|
103
|
+
"""Return a filesystem-safe file name for ``command``."""
|
|
104
|
+
safe = "".join(ch if ch.isalnum() or ch in "-_." else "_" for ch in command)
|
|
105
|
+
return f"{safe}.json"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _entry_path(base_url: str, command: str) -> Optional[Path]:
|
|
109
|
+
"""Return the cache file path for (base_url, command), or None if disabled/invalid.
|
|
110
|
+
|
|
111
|
+
``base_url`` must be a real string (a mock or other stand-in used by
|
|
112
|
+
tests is not, and is deliberately treated as "caching unavailable" so
|
|
113
|
+
unit tests exercising a mocked transport never touch the filesystem).
|
|
114
|
+
"""
|
|
115
|
+
if not isinstance(base_url, str) or not base_url or not isinstance(command, str):
|
|
116
|
+
return None
|
|
117
|
+
root = _cache_root()
|
|
118
|
+
if root is None:
|
|
119
|
+
return None
|
|
120
|
+
return _server_dir(root, base_url) / _safe_command_filename(command)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def load_cached_schema(base_url: str, command: str) -> Optional[Dict[str, Any]]:
|
|
124
|
+
"""Return a fresh-enough disk-cached schema for (base_url, command), else None.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
base_url: The server's base URL (``JsonRpcClient.base_url``), used as
|
|
128
|
+
the cache partition key.
|
|
129
|
+
command: Command name whose schema is wanted.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
The cached schema dict when a readable, unexpired entry exists;
|
|
133
|
+
``None`` on any cache miss, staleness, or I/O/parse failure.
|
|
134
|
+
"""
|
|
135
|
+
path = _entry_path(base_url, command)
|
|
136
|
+
if path is None:
|
|
137
|
+
return None
|
|
138
|
+
try:
|
|
139
|
+
if not path.exists():
|
|
140
|
+
return None
|
|
141
|
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
142
|
+
except (OSError, ValueError):
|
|
143
|
+
return None
|
|
144
|
+
if not isinstance(raw, dict):
|
|
145
|
+
return None
|
|
146
|
+
fetched_at = raw.get("fetched_at")
|
|
147
|
+
schema = raw.get("schema")
|
|
148
|
+
if not isinstance(fetched_at, (int, float)) or not isinstance(schema, dict):
|
|
149
|
+
return None
|
|
150
|
+
if (time.time() - fetched_at) > _ttl_seconds():
|
|
151
|
+
return None
|
|
152
|
+
return schema
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def store_cached_schema(base_url: str, command: str, schema: Dict[str, Any]) -> None:
|
|
156
|
+
"""Best-effort persist ``schema`` for (base_url, command) to local disk.
|
|
157
|
+
|
|
158
|
+
Writes to a process-unique temp file first and renames it over the final
|
|
159
|
+
path (atomic on the same filesystem) so a concurrent reader never sees a
|
|
160
|
+
partially written file. Never raises: a write failure only forfeits the
|
|
161
|
+
speedup, it must not break the calling command.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
base_url: The server's base URL, used as the cache partition key.
|
|
165
|
+
command: Command name the schema belongs to.
|
|
166
|
+
schema: The schema dict to persist (as returned by the server's
|
|
167
|
+
``help`` command for this command name).
|
|
168
|
+
"""
|
|
169
|
+
path = _entry_path(base_url, command)
|
|
170
|
+
if path is None:
|
|
171
|
+
return
|
|
172
|
+
try:
|
|
173
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
174
|
+
tmp_path = path.with_name(f".{path.name}.{os.getpid()}.tmp")
|
|
175
|
+
tmp_path.write_text(
|
|
176
|
+
json.dumps({"fetched_at": time.time(), "schema": schema}),
|
|
177
|
+
encoding="utf-8",
|
|
178
|
+
)
|
|
179
|
+
tmp_path.replace(path)
|
|
180
|
+
except OSError:
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def clear_cached_schemas(base_url: str) -> None:
|
|
185
|
+
"""Best-effort delete every on-disk cached schema entry for ``base_url``.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
base_url: The server's base URL whose cached entries should be
|
|
189
|
+
forgotten (e.g. after the caller learns the server reloaded its
|
|
190
|
+
command definitions).
|
|
191
|
+
"""
|
|
192
|
+
if not isinstance(base_url, str) or not base_url:
|
|
193
|
+
return
|
|
194
|
+
root = _cache_root()
|
|
195
|
+
if root is None:
|
|
196
|
+
return
|
|
197
|
+
server_dir = _server_dir(root, base_url)
|
|
198
|
+
try:
|
|
199
|
+
if not server_dir.exists():
|
|
200
|
+
return
|
|
201
|
+
for entry in server_dir.iterdir():
|
|
202
|
+
try:
|
|
203
|
+
entry.unlink()
|
|
204
|
+
except OSError:
|
|
205
|
+
continue
|
|
206
|
+
except OSError:
|
|
207
|
+
return
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Canonical command names for the high-level client API vs the live server registry.
|
|
3
|
+
|
|
4
|
+
**code-analysis-server** owns project trees, the index database, sessions, locks,
|
|
5
|
+
and editor ingress (chunked transfer). It does **not** expose in-server content
|
|
6
|
+
editing (``universal_file_open/edit/write/close``, CST/JSON tree modify/save,
|
|
7
|
+
``format_code``, legacy line writers).
|
|
8
|
+
|
|
9
|
+
**File content on CA** — read-only surface only:
|
|
10
|
+
|
|
11
|
+
- :data:`FILE_CONTENT_READ_COMMANDS` — preview and project search (``search`` /
|
|
12
|
+
``search_get_page``); on-disk grep is internal to ``search`` when enabled.
|
|
13
|
+
- :data:`FS_COMMANDS` — filesystem operations (copy/move/remove/list/grep); not
|
|
14
|
+
the structured edit workflow.
|
|
15
|
+
|
|
16
|
+
Editor clients use :data:`CLIENT_FACADE_COMMANDS` (sessions, transfer, locks,
|
|
17
|
+
``universal_file_preview``).
|
|
18
|
+
|
|
19
|
+
Author: Vasiliy Zdanovskiy
|
|
20
|
+
email: vasilyvz@gmail.com
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from typing import Callable, Dict, FrozenSet, Tuple
|
|
26
|
+
|
|
27
|
+
# Removed from the public server surface — do not use in client facades or docs.
|
|
28
|
+
LEGACY_REMOVED_COMMANDS: FrozenSet[str] = frozenset(
|
|
29
|
+
{
|
|
30
|
+
"universal_file_read",
|
|
31
|
+
"universal_file_save",
|
|
32
|
+
"universal_file_replace",
|
|
33
|
+
"universal_file_delete",
|
|
34
|
+
"read_project_text_file",
|
|
35
|
+
"write_project_text_lines",
|
|
36
|
+
"create_text_file",
|
|
37
|
+
"replace_file_lines",
|
|
38
|
+
"universal_file_search",
|
|
39
|
+
# TODO a7091850: removed from the public surface. GetFileLinesCommand
|
|
40
|
+
# still exists as an internal (unregistered) helper for
|
|
41
|
+
# read_project_text_file / universal_file_read Python-ecosystem
|
|
42
|
+
# line-range routing.
|
|
43
|
+
"get_file_lines",
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# CST / JSON tree MCP workflow — modules may exist in the repo but are not registered.
|
|
48
|
+
CST_REMOVED_COMMANDS: FrozenSet[str] = frozenset(
|
|
49
|
+
{
|
|
50
|
+
"cst_load_file",
|
|
51
|
+
"cst_save_tree",
|
|
52
|
+
"cst_modify_tree",
|
|
53
|
+
"cst_create_file",
|
|
54
|
+
"cst_find_node",
|
|
55
|
+
"cst_get_node_info",
|
|
56
|
+
"cst_get_node_by_range",
|
|
57
|
+
"cst_get_node_at_line",
|
|
58
|
+
"cst_reload_tree",
|
|
59
|
+
"cst_convert_and_save",
|
|
60
|
+
"cst_apply_buffer",
|
|
61
|
+
"cst_list_trees",
|
|
62
|
+
"cst_unload_tree",
|
|
63
|
+
"list_cst_blocks",
|
|
64
|
+
"query_cst",
|
|
65
|
+
"json_load_file",
|
|
66
|
+
"json_find_node",
|
|
67
|
+
"json_get_node_info",
|
|
68
|
+
"json_modify_tree",
|
|
69
|
+
"json_save_tree",
|
|
70
|
+
"json_reload_tree",
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Content editing on ai-editor-server only (not on code-analysis-server).
|
|
75
|
+
EDITING_REMOVED_COMMANDS: FrozenSet[str] = frozenset(
|
|
76
|
+
{
|
|
77
|
+
"universal_file_open",
|
|
78
|
+
"universal_file_edit",
|
|
79
|
+
"universal_file_write",
|
|
80
|
+
"universal_file_close",
|
|
81
|
+
"universal_file_move_nodes",
|
|
82
|
+
"session_git_log",
|
|
83
|
+
"session_git_diff",
|
|
84
|
+
"session_git_show",
|
|
85
|
+
"session_git_status",
|
|
86
|
+
"session_git_revert",
|
|
87
|
+
"session_undo",
|
|
88
|
+
"session_redo",
|
|
89
|
+
"session_write",
|
|
90
|
+
# Bug 9ba69ede-a5fc-416a-a4db-f91fffad5aa2 (K6, commit 958c266c) re-registered delete_file, unmark_deleted_file,
|
|
91
|
+
# delete_files_by_mask, restore_deleted_files and cleanup_deleted_files; restore_backup_file, delete_backup and
|
|
92
|
+
# clear_all_backups are registered in hooks_register_part2 (F5 backup contract). They are served commands, so
|
|
93
|
+
# they are no longer listed here (live-deployed-server reported them as DISCREPANCY: listed removed but live-present).
|
|
94
|
+
"collapse_versions",
|
|
95
|
+
"format_code",
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
REMOVED_COMMANDS: FrozenSet[str] = (
|
|
100
|
+
LEGACY_REMOVED_COMMANDS | CST_REMOVED_COMMANDS | EDITING_REMOVED_COMMANDS
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Read-only file **content** on CA (no mutate/edit workflow). Includes on-disk search
|
|
104
|
+
# (``fs_grep``, paginated ``search_*``) for paths not yet in the index.
|
|
105
|
+
FILE_CONTENT_READ_COMMANDS: FrozenSet[str] = frozenset(
|
|
106
|
+
{
|
|
107
|
+
"universal_file_preview",
|
|
108
|
+
"search",
|
|
109
|
+
"search_get_page",
|
|
110
|
+
"search_get_status",
|
|
111
|
+
"search_cancel",
|
|
112
|
+
"search_close",
|
|
113
|
+
}
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Filesystem helpers — separate from structured content editing; may touch paths/bytes
|
|
117
|
+
# via copy/move/remove but are not universal_file_edit / CST / format_code.
|
|
118
|
+
FS_COMMANDS: FrozenSet[str] = frozenset(
|
|
119
|
+
{
|
|
120
|
+
"fs_copy",
|
|
121
|
+
"fs_move",
|
|
122
|
+
"fs_remove",
|
|
123
|
+
"cleanup_tree_sidecars",
|
|
124
|
+
}
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Client sessions + DB file locks — persisted on this server for editor/terminal clients.
|
|
128
|
+
FILE_SESSION_COMMANDS: FrozenSet[str] = frozenset(
|
|
129
|
+
{
|
|
130
|
+
"session_create",
|
|
131
|
+
"session_validate",
|
|
132
|
+
"session_delete",
|
|
133
|
+
"session_worktrees_delete",
|
|
134
|
+
"session_list",
|
|
135
|
+
"session_view",
|
|
136
|
+
"session_open_file",
|
|
137
|
+
"session_open_documentation_document",
|
|
138
|
+
"session_close_file",
|
|
139
|
+
"session_list_file_locks",
|
|
140
|
+
"list_documentation_orphans",
|
|
141
|
+
"subordinate_session_create",
|
|
142
|
+
"subordinate_session_get",
|
|
143
|
+
"subordinate_session_update",
|
|
144
|
+
"subordinate_session_delete",
|
|
145
|
+
"subordinate_session_list",
|
|
146
|
+
}
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
TRANSFER_AND_LOCK_COMMANDS: FrozenSet[str] = frozenset(
|
|
150
|
+
{
|
|
151
|
+
"project_file_transfer_download_begin",
|
|
152
|
+
"project_file_transfer_upload_save",
|
|
153
|
+
"project_file_advisory_lock_batch",
|
|
154
|
+
"file_lock_write_check",
|
|
155
|
+
}
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
UNIVERSAL_FILE_COMMANDS: FrozenSet[str] = frozenset(
|
|
159
|
+
{
|
|
160
|
+
"universal_file_preview",
|
|
161
|
+
}
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
CLIENT_FACADE_COMMANDS: FrozenSet[str] = (
|
|
165
|
+
FILE_SESSION_COMMANDS | TRANSFER_AND_LOCK_COMMANDS | UNIVERSAL_FILE_COMMANDS
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# ``FileSessionClient`` method names for each ``FILE_SESSION_COMMANDS`` entry.
|
|
169
|
+
# ``session_list_file_locks`` is also used by ``assert_session_exists``.
|
|
170
|
+
FILE_SESSION_FACADE_METHODS: Dict[str, str] = {
|
|
171
|
+
"session_create": "create_session",
|
|
172
|
+
"session_validate": "validate_session",
|
|
173
|
+
"session_delete": "delete_session",
|
|
174
|
+
"session_worktrees_delete": "session_worktrees_delete",
|
|
175
|
+
"session_list": "list_sessions",
|
|
176
|
+
"session_view": "view_session",
|
|
177
|
+
"session_open_file": "lock_file",
|
|
178
|
+
"session_open_documentation_document": "open_documentation_document",
|
|
179
|
+
"list_documentation_orphans": "list_documentation_orphans",
|
|
180
|
+
"session_close_file": "unlock_file",
|
|
181
|
+
"session_list_file_locks": "list_file_locks",
|
|
182
|
+
"subordinate_session_create": "create_subordinate_session",
|
|
183
|
+
"subordinate_session_get": "get_subordinate_session",
|
|
184
|
+
"subordinate_session_update": "update_subordinate_session",
|
|
185
|
+
"subordinate_session_delete": "delete_subordinate_session",
|
|
186
|
+
"subordinate_session_list": "list_subordinate_sessions",
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
# Transfer / advisory-lock commands may map to more than one façade method.
|
|
190
|
+
TRANSFER_FACADE_METHODS: Dict[str, Tuple[str, ...]] = {
|
|
191
|
+
"project_file_transfer_download_begin": ("download",),
|
|
192
|
+
"project_file_transfer_upload_save": ("upload", "upload_new"),
|
|
193
|
+
"project_file_advisory_lock_batch": (
|
|
194
|
+
"lock_files_advisory",
|
|
195
|
+
"unlock_files_advisory",
|
|
196
|
+
),
|
|
197
|
+
"file_lock_write_check": ("file_lock_write_check",),
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def assert_file_session_facade_complete() -> None:
|
|
202
|
+
"""Raise ``AssertionError`` if ``FILE_SESSION_FACADE_METHODS`` is incomplete."""
|
|
203
|
+
missing_cmds = FILE_SESSION_COMMANDS - set(FILE_SESSION_FACADE_METHODS)
|
|
204
|
+
extra_cmds = set(FILE_SESSION_FACADE_METHODS) - FILE_SESSION_COMMANDS
|
|
205
|
+
if missing_cmds or extra_cmds:
|
|
206
|
+
raise AssertionError(
|
|
207
|
+
"FILE_SESSION_FACADE_METHODS out of sync with FILE_SESSION_COMMANDS: "
|
|
208
|
+
f"missing={sorted(missing_cmds)!r} extra={sorted(extra_cmds)!r}"
|
|
209
|
+
)
|
|
210
|
+
from code_analysis_client.file_session import FileSessionClient
|
|
211
|
+
|
|
212
|
+
for command, method_name in FILE_SESSION_FACADE_METHODS.items():
|
|
213
|
+
if not hasattr(FileSessionClient, method_name):
|
|
214
|
+
raise AssertionError(
|
|
215
|
+
f"FileSessionClient missing method {method_name!r} for command {command!r}"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def assert_transfer_facade_complete() -> None:
|
|
220
|
+
"""Raise ``AssertionError`` if ``TRANSFER_FACADE_METHODS`` is incomplete."""
|
|
221
|
+
missing_cmds = TRANSFER_AND_LOCK_COMMANDS - set(TRANSFER_FACADE_METHODS)
|
|
222
|
+
extra_cmds = set(TRANSFER_FACADE_METHODS) - TRANSFER_AND_LOCK_COMMANDS
|
|
223
|
+
if missing_cmds or extra_cmds:
|
|
224
|
+
raise AssertionError(
|
|
225
|
+
"TRANSFER_FACADE_METHODS out of sync with TRANSFER_AND_LOCK_COMMANDS: "
|
|
226
|
+
f"missing={sorted(missing_cmds)!r} extra={sorted(extra_cmds)!r}"
|
|
227
|
+
)
|
|
228
|
+
from code_analysis_client.file_session import FileSessionClient
|
|
229
|
+
|
|
230
|
+
for command, method_names in TRANSFER_FACADE_METHODS.items():
|
|
231
|
+
for method_name in method_names:
|
|
232
|
+
if not hasattr(FileSessionClient, method_name):
|
|
233
|
+
raise AssertionError(
|
|
234
|
+
f"FileSessionClient missing method {method_name!r} "
|
|
235
|
+
f"for command {command!r}"
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _command_registered(get_command: Callable[[str], object], name: str) -> bool:
|
|
240
|
+
"""Return True when ``get_command`` can resolve a command name."""
|
|
241
|
+
try:
|
|
242
|
+
get_command(name)
|
|
243
|
+
return True
|
|
244
|
+
except KeyError:
|
|
245
|
+
return False
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def assert_facade_commands_registered(get_command: Callable[[str], object]) -> None:
|
|
249
|
+
"""Raise ``KeyError`` if any facade command is missing from the server registry."""
|
|
250
|
+
missing = [
|
|
251
|
+
name
|
|
252
|
+
for name in sorted(CLIENT_FACADE_COMMANDS)
|
|
253
|
+
if not _command_registered(get_command, name)
|
|
254
|
+
]
|
|
255
|
+
if missing:
|
|
256
|
+
raise KeyError(f"Client facade commands not registered on server: {missing}")
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def assert_removed_commands_absent(get_command: Callable[[str], object]) -> None:
|
|
260
|
+
"""Raise ``AssertionError`` if a removed command is still registered."""
|
|
261
|
+
present = [
|
|
262
|
+
name
|
|
263
|
+
for name in sorted(REMOVED_COMMANDS)
|
|
264
|
+
if _command_registered(get_command, name)
|
|
265
|
+
]
|
|
266
|
+
if present:
|
|
267
|
+
raise AssertionError(f"Removed commands still registered: {present}")
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def assert_file_content_read_commands_registered(
|
|
271
|
+
get_command: Callable[[str], object],
|
|
272
|
+
) -> None:
|
|
273
|
+
"""Raise ``KeyError`` if a read-only content command is missing from the registry."""
|
|
274
|
+
missing = [
|
|
275
|
+
name
|
|
276
|
+
for name in sorted(FILE_CONTENT_READ_COMMANDS)
|
|
277
|
+
if not _command_registered(get_command, name)
|
|
278
|
+
]
|
|
279
|
+
if missing:
|
|
280
|
+
raise KeyError(f"File content read commands not registered: {missing}")
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def assert_fs_commands_registered(get_command: Callable[[str], object]) -> None:
|
|
284
|
+
"""Raise ``KeyError`` if a filesystem helper command is missing from the registry."""
|
|
285
|
+
missing = [
|
|
286
|
+
name
|
|
287
|
+
for name in sorted(FS_COMMANDS)
|
|
288
|
+
if not _command_registered(get_command, name)
|
|
289
|
+
]
|
|
290
|
+
if missing:
|
|
291
|
+
raise KeyError(f"Filesystem commands not registered: {missing}")
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Load per-command JSON schema from the running server via ``help`` (cmdname).
|
|
3
|
+
|
|
4
|
+
Author: Vasiliy Zdanovskiy
|
|
5
|
+
email: vasilyvz@gmail.com
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any, Dict
|
|
11
|
+
|
|
12
|
+
from code_analysis_client.exceptions import ClientValidationError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def parse_schema_from_help_payload(
|
|
16
|
+
payload: Dict[str, Any], *, command_name: str
|
|
17
|
+
) -> Dict[str, Any]:
|
|
18
|
+
"""Extract ``get_schema()``-style dict from ``help`` JSON-RPC result."""
|
|
19
|
+
if not payload.get("success"):
|
|
20
|
+
raise ClientValidationError(
|
|
21
|
+
f"{command_name}: help returned success=false: {payload!r}",
|
|
22
|
+
field="help",
|
|
23
|
+
details={"payload": payload},
|
|
24
|
+
)
|
|
25
|
+
data = payload.get("data")
|
|
26
|
+
if not isinstance(data, dict):
|
|
27
|
+
raise ClientValidationError(
|
|
28
|
+
f"{command_name}: help response missing object 'data'",
|
|
29
|
+
field="help",
|
|
30
|
+
)
|
|
31
|
+
if data.get("error") and "schema" not in data:
|
|
32
|
+
raise ClientValidationError(
|
|
33
|
+
f"{command_name}: {data.get('error')}",
|
|
34
|
+
field="help",
|
|
35
|
+
details={"note": data.get("note"), "example": data.get("example")},
|
|
36
|
+
)
|
|
37
|
+
schema = data.get("schema")
|
|
38
|
+
if not isinstance(schema, dict):
|
|
39
|
+
raise ClientValidationError(
|
|
40
|
+
f"{command_name}: help response has no 'schema' object",
|
|
41
|
+
field="help",
|
|
42
|
+
)
|
|
43
|
+
return schema
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def fetch_command_schema_from_server(
|
|
47
|
+
rpc: Any,
|
|
48
|
+
command_name: str,
|
|
49
|
+
) -> Dict[str, Any]:
|
|
50
|
+
"""Call server ``help`` with ``cmdname`` and return that command's input schema."""
|
|
51
|
+
raw = await rpc.help(command_name)
|
|
52
|
+
if not isinstance(raw, dict):
|
|
53
|
+
raise ClientValidationError(
|
|
54
|
+
f"{command_name}: help returned non-dict: {type(raw).__name__}",
|
|
55
|
+
field="help",
|
|
56
|
+
)
|
|
57
|
+
return parse_schema_from_help_payload(raw, command_name=command_name)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""The uniform recursive declaration-tree result."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TypedDict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SymbolTreeNode(TypedDict):
|
|
9
|
+
"""One declaration; kinds belong to language plugins."""
|
|
10
|
+
|
|
11
|
+
name: str
|
|
12
|
+
kind: str
|
|
13
|
+
children: list[SymbolTreeNode]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Read-only structured file preview on top of :class:`CodeAnalysisAsyncClient`.
|
|
3
|
+
|
|
4
|
+
Wraps ``universal_file_preview`` only: a preview-only surface (project id and
|
|
5
|
+
file path plus navigation parameters). Content editing is not served by this
|
|
6
|
+
project's code-analysis server; that workflow lives in the ai-editor client.
|
|
7
|
+
|
|
8
|
+
Author: Vasiliy Zdanovskiy
|
|
9
|
+
email: vasilyvz@gmail.com
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import TYPE_CHECKING, Any, Dict
|
|
15
|
+
|
|
16
|
+
from code_analysis_client.responses import unwrap_command_result, FacadeCaller
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from code_analysis_client.client import CodeAnalysisAsyncClient
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class UniversalFileClient:
|
|
23
|
+
"""Read-only structured preview for project files.
|
|
24
|
+
|
|
25
|
+
Wraps ``universal_file_preview`` (the only ``universal_file_*`` command
|
|
26
|
+
registered on the code-analysis server). Content editing belongs to the
|
|
27
|
+
ai-editor client, not this package.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
__slots__ = ("_client",)
|
|
31
|
+
|
|
32
|
+
def __init__(self, client: CodeAnalysisAsyncClient) -> None:
|
|
33
|
+
"""Store the async client used for universal file preview commands."""
|
|
34
|
+
self._client = FacadeCaller(client) # type: ignore[assignment]
|
|
35
|
+
|
|
36
|
+
async def preview(
|
|
37
|
+
self,
|
|
38
|
+
project_id: str,
|
|
39
|
+
file_path: str,
|
|
40
|
+
**kwargs: Any,
|
|
41
|
+
) -> Dict[str, Any]:
|
|
42
|
+
"""Structured read-only preview (``universal_file_preview``)."""
|
|
43
|
+
params: Dict[str, Any] = {
|
|
44
|
+
"project_id": project_id,
|
|
45
|
+
"file_path": file_path,
|
|
46
|
+
}
|
|
47
|
+
params.update(kwargs)
|
|
48
|
+
return unwrap_command_result(
|
|
49
|
+
await self._client.call_validated("universal_file_preview", params)
|
|
50
|
+
)
|