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.
@@ -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
+ )