code-analysis-client 1.6.94__tar.gz → 1.6.95__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.6.94 → code_analysis_client-1.6.95}/PKG-INFO +1 -1
- {code_analysis_client-1.6.94 → code_analysis_client-1.6.95}/code_analysis_client/client.py +31 -2
- code_analysis_client-1.6.95/code_analysis_client/schema_disk_cache.py +207 -0
- code_analysis_client-1.6.95/code_analysis_client/version.txt +1 -0
- {code_analysis_client-1.6.94 → code_analysis_client-1.6.95}/code_analysis_client.egg-info/PKG-INFO +1 -1
- {code_analysis_client-1.6.94 → code_analysis_client-1.6.95}/code_analysis_client.egg-info/SOURCES.txt +1 -0
- code_analysis_client-1.6.94/code_analysis_client/version.txt +0 -1
- {code_analysis_client-1.6.94 → code_analysis_client-1.6.95}/README.md +0 -0
- {code_analysis_client-1.6.94 → code_analysis_client-1.6.95}/code_analysis_client/__init__.py +0 -0
- {code_analysis_client-1.6.94 → code_analysis_client-1.6.95}/code_analysis_client/commands_proxy.py +0 -0
- {code_analysis_client-1.6.94 → code_analysis_client-1.6.95}/code_analysis_client/config.py +0 -0
- {code_analysis_client-1.6.94 → code_analysis_client-1.6.95}/code_analysis_client/exceptions.py +0 -0
- {code_analysis_client-1.6.94 → code_analysis_client-1.6.95}/code_analysis_client/file_session.py +0 -0
- {code_analysis_client-1.6.94 → code_analysis_client-1.6.95}/code_analysis_client/py.typed +0 -0
- {code_analysis_client-1.6.94 → code_analysis_client-1.6.95}/code_analysis_client/queue_wait.py +0 -0
- {code_analysis_client-1.6.94 → code_analysis_client-1.6.95}/code_analysis_client/responses.py +0 -0
- {code_analysis_client-1.6.94 → code_analysis_client-1.6.95}/code_analysis_client/server_api.py +0 -0
- {code_analysis_client-1.6.94 → code_analysis_client-1.6.95}/code_analysis_client/server_schema.py +0 -0
- {code_analysis_client-1.6.94 → code_analysis_client-1.6.95}/code_analysis_client/universal_file.py +0 -0
- {code_analysis_client-1.6.94 → code_analysis_client-1.6.95}/code_analysis_client/validation.py +0 -0
- {code_analysis_client-1.6.94 → code_analysis_client-1.6.95}/code_analysis_client.egg-info/dependency_links.txt +0 -0
- {code_analysis_client-1.6.94 → code_analysis_client-1.6.95}/code_analysis_client.egg-info/requires.txt +0 -0
- {code_analysis_client-1.6.94 → code_analysis_client-1.6.95}/code_analysis_client.egg-info/top_level.txt +0 -0
- {code_analysis_client-1.6.94 → code_analysis_client-1.6.95}/pyproject.toml +0 -0
- {code_analysis_client-1.6.94 → code_analysis_client-1.6.95}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-analysis-client
|
|
3
|
-
Version: 1.6.
|
|
3
|
+
Version: 1.6.95
|
|
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
|
|
@@ -32,6 +32,11 @@ from code_analysis_client.queue_wait import (
|
|
|
32
32
|
wait_for_job,
|
|
33
33
|
)
|
|
34
34
|
from code_analysis_client.universal_file import UniversalFileClient
|
|
35
|
+
from code_analysis_client.schema_disk_cache import (
|
|
36
|
+
clear_cached_schemas,
|
|
37
|
+
load_cached_schema,
|
|
38
|
+
store_cached_schema,
|
|
39
|
+
)
|
|
35
40
|
from code_analysis_client.server_schema import fetch_command_schema_from_server
|
|
36
41
|
from code_analysis_client.validation import (
|
|
37
42
|
prepare_params_for_schema,
|
|
@@ -127,8 +132,15 @@ class CodeAnalysisAsyncClient:
|
|
|
127
132
|
return self._commands_proxy
|
|
128
133
|
|
|
129
134
|
def clear_command_schema_cache(self) -> None:
|
|
130
|
-
"""Drop cached command schemas (after ``reload`` or when server definitions change).
|
|
135
|
+
"""Drop cached command schemas (after ``reload`` or when server definitions change).
|
|
136
|
+
|
|
137
|
+
Clears both the in-memory cache and this server's on-disk schema
|
|
138
|
+
cache (:mod:`code_analysis_client.schema_disk_cache`, bug 8e6acb34) —
|
|
139
|
+
otherwise the very next call would just repopulate the in-memory
|
|
140
|
+
cache from a now-stale disk entry, defeating the point of clearing it.
|
|
141
|
+
"""
|
|
131
142
|
self._command_schema_cache.clear()
|
|
143
|
+
clear_cached_schemas(self._rpc.base_url)
|
|
132
144
|
|
|
133
145
|
@property
|
|
134
146
|
def file_sessions(self) -> FileSessionClient:
|
|
@@ -143,11 +155,28 @@ class CodeAnalysisAsyncClient:
|
|
|
143
155
|
async def get_command_schema(
|
|
144
156
|
self, command: str, *, refresh: bool = False
|
|
145
157
|
) -> Dict[str, Any]:
|
|
146
|
-
"""Fetch input JSON schema for ``command`` using server ``help
|
|
158
|
+
"""Fetch input JSON schema for ``command`` using server ``help``.
|
|
159
|
+
|
|
160
|
+
Checked in order (bug 8e6acb34, Fix 1): the in-memory per-instance
|
|
161
|
+
cache, then a bounded-TTL on-disk cache shared across process
|
|
162
|
+
invocations against the same server
|
|
163
|
+
(:mod:`code_analysis_client.schema_disk_cache`) — this is what
|
|
164
|
+
removes the ~2.7ms ``help`` round trip that a fresh client instance
|
|
165
|
+
would otherwise pay on every first use of a given command, since most
|
|
166
|
+
real callers are short-lived processes that never benefit from an
|
|
167
|
+
in-memory-only cache. ``refresh=True`` skips both caches and always
|
|
168
|
+
re-fetches from the server, repopulating them afterward.
|
|
169
|
+
"""
|
|
147
170
|
if not refresh and command in self._command_schema_cache:
|
|
148
171
|
return self._command_schema_cache[command]
|
|
172
|
+
if not refresh:
|
|
173
|
+
cached = load_cached_schema(self._rpc.base_url, command)
|
|
174
|
+
if cached is not None:
|
|
175
|
+
self._command_schema_cache[command] = cached
|
|
176
|
+
return cached
|
|
149
177
|
schema = await fetch_command_schema_from_server(self._rpc, command)
|
|
150
178
|
self._command_schema_cache[command] = schema
|
|
179
|
+
store_cached_schema(self._rpc.base_url, command, schema)
|
|
151
180
|
return schema
|
|
152
181
|
|
|
153
182
|
async def _execute(
|
|
@@ -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 @@
|
|
|
1
|
+
1.6.95
|
{code_analysis_client-1.6.94 → code_analysis_client-1.6.95}/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.6.
|
|
3
|
+
Version: 1.6.95
|
|
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
|
|
@@ -9,6 +9,7 @@ code_analysis_client/file_session.py
|
|
|
9
9
|
code_analysis_client/py.typed
|
|
10
10
|
code_analysis_client/queue_wait.py
|
|
11
11
|
code_analysis_client/responses.py
|
|
12
|
+
code_analysis_client/schema_disk_cache.py
|
|
12
13
|
code_analysis_client/server_api.py
|
|
13
14
|
code_analysis_client/server_schema.py
|
|
14
15
|
code_analysis_client/universal_file.py
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
1.6.94
|
|
File without changes
|
{code_analysis_client-1.6.94 → code_analysis_client-1.6.95}/code_analysis_client/__init__.py
RENAMED
|
File without changes
|
{code_analysis_client-1.6.94 → code_analysis_client-1.6.95}/code_analysis_client/commands_proxy.py
RENAMED
|
File without changes
|
|
File without changes
|
{code_analysis_client-1.6.94 → code_analysis_client-1.6.95}/code_analysis_client/exceptions.py
RENAMED
|
File without changes
|
{code_analysis_client-1.6.94 → code_analysis_client-1.6.95}/code_analysis_client/file_session.py
RENAMED
|
File without changes
|
|
File without changes
|
{code_analysis_client-1.6.94 → code_analysis_client-1.6.95}/code_analysis_client/queue_wait.py
RENAMED
|
File without changes
|
{code_analysis_client-1.6.94 → code_analysis_client-1.6.95}/code_analysis_client/responses.py
RENAMED
|
File without changes
|
{code_analysis_client-1.6.94 → code_analysis_client-1.6.95}/code_analysis_client/server_api.py
RENAMED
|
File without changes
|
{code_analysis_client-1.6.94 → code_analysis_client-1.6.95}/code_analysis_client/server_schema.py
RENAMED
|
File without changes
|
{code_analysis_client-1.6.94 → code_analysis_client-1.6.95}/code_analysis_client/universal_file.py
RENAMED
|
File without changes
|
{code_analysis_client-1.6.94 → code_analysis_client-1.6.95}/code_analysis_client/validation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|