code-analysis-client 1.6.148__tar.gz → 1.6.151__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.148 → code_analysis_client-1.6.151}/PKG-INFO +1 -1
- {code_analysis_client-1.6.148 → code_analysis_client-1.6.151}/code_analysis_client/queue_wait.py +102 -14
- code_analysis_client-1.6.151/code_analysis_client/version.txt +1 -0
- {code_analysis_client-1.6.148 → code_analysis_client-1.6.151}/code_analysis_client.egg-info/PKG-INFO +1 -1
- code_analysis_client-1.6.148/code_analysis_client/version.txt +0 -1
- {code_analysis_client-1.6.148 → code_analysis_client-1.6.151}/README.md +0 -0
- {code_analysis_client-1.6.148 → code_analysis_client-1.6.151}/code_analysis_client/__init__.py +0 -0
- {code_analysis_client-1.6.148 → code_analysis_client-1.6.151}/code_analysis_client/client.py +0 -0
- {code_analysis_client-1.6.148 → code_analysis_client-1.6.151}/code_analysis_client/commands_proxy.py +0 -0
- {code_analysis_client-1.6.148 → code_analysis_client-1.6.151}/code_analysis_client/config.py +0 -0
- {code_analysis_client-1.6.148 → code_analysis_client-1.6.151}/code_analysis_client/exceptions.py +0 -0
- {code_analysis_client-1.6.148 → code_analysis_client-1.6.151}/code_analysis_client/file_session.py +0 -0
- {code_analysis_client-1.6.148 → code_analysis_client-1.6.151}/code_analysis_client/py.typed +0 -0
- {code_analysis_client-1.6.148 → code_analysis_client-1.6.151}/code_analysis_client/responses.py +0 -0
- {code_analysis_client-1.6.148 → code_analysis_client-1.6.151}/code_analysis_client/schema_disk_cache.py +0 -0
- {code_analysis_client-1.6.148 → code_analysis_client-1.6.151}/code_analysis_client/server_api.py +0 -0
- {code_analysis_client-1.6.148 → code_analysis_client-1.6.151}/code_analysis_client/server_schema.py +0 -0
- {code_analysis_client-1.6.148 → code_analysis_client-1.6.151}/code_analysis_client/universal_file.py +0 -0
- {code_analysis_client-1.6.148 → code_analysis_client-1.6.151}/code_analysis_client/validation.py +0 -0
- {code_analysis_client-1.6.148 → code_analysis_client-1.6.151}/code_analysis_client.egg-info/SOURCES.txt +0 -0
- {code_analysis_client-1.6.148 → code_analysis_client-1.6.151}/code_analysis_client.egg-info/dependency_links.txt +0 -0
- {code_analysis_client-1.6.148 → code_analysis_client-1.6.151}/code_analysis_client.egg-info/requires.txt +0 -0
- {code_analysis_client-1.6.148 → code_analysis_client-1.6.151}/code_analysis_client.egg-info/top_level.txt +0 -0
- {code_analysis_client-1.6.148 → code_analysis_client-1.6.151}/pyproject.toml +0 -0
- {code_analysis_client-1.6.148 → code_analysis_client-1.6.151}/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.151
|
|
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
|
{code_analysis_client-1.6.148 → code_analysis_client-1.6.151}/code_analysis_client/queue_wait.py
RENAMED
|
@@ -156,7 +156,7 @@ async def wait_for_job(
|
|
|
156
156
|
await asyncio.sleep(poll_interval)
|
|
157
157
|
|
|
158
158
|
|
|
159
|
-
_JOB_FAILED_REFETCH_ATTEMPTS =
|
|
159
|
+
_JOB_FAILED_REFETCH_ATTEMPTS = 8
|
|
160
160
|
_JOB_FAILED_REFETCH_SLEEP_SECONDS = 0.5
|
|
161
161
|
|
|
162
162
|
|
|
@@ -179,7 +179,83 @@ def _extract_inner_command_error(status_data: Dict[str, Any]) -> Any:
|
|
|
179
179
|
return None
|
|
180
180
|
|
|
181
181
|
|
|
182
|
-
async def
|
|
182
|
+
async def _refetch_job_status_with_payload(
|
|
183
|
+
rpc: Any, job_id: str, original_data: Optional[Dict[str, Any]] = None
|
|
184
|
+
) -> Optional[Dict[str, Any]]:
|
|
185
|
+
"""Re-poll ``queue_get_job_status`` hunting for changes in status or result payload.
|
|
186
|
+
|
|
187
|
+
Write-order gotcha: the queue store writes the job's terminal ``status``
|
|
188
|
+
slightly BEFORE it writes the ``result`` payload (or in some cases, the
|
|
189
|
+
status may transition from one state to another). A caller that reads the
|
|
190
|
+
status at the instant it turns terminal can therefore observe one of:
|
|
191
|
+
* A status change that hasn't been re-read yet (e.g. completed->failed)
|
|
192
|
+
* A terminal status with no ``result`` payload yet, even though the
|
|
193
|
+
command DID record a structured error - it just hasn't landed.
|
|
194
|
+
|
|
195
|
+
This helper re-fetches the job status multiple times, returning the FIRST
|
|
196
|
+
snapshot where EITHER:
|
|
197
|
+
* The status has CHANGED relative to the original snapshot
|
|
198
|
+
* A ``result`` payload has APPEARED or CHANGED (was missing/empty, now populated)
|
|
199
|
+
* An inner structured error has APPEARED or CHANGED (nested error found)
|
|
200
|
+
* A top-level error field has APPEARED or CHANGED (not in original or different value)
|
|
201
|
+
|
|
202
|
+
If ``original_data`` is provided, each new snapshot is compared against it.
|
|
203
|
+
Snapshots that are identical to the original (same status, same error, same result)
|
|
204
|
+
do NOT trigger early exit — the loop continues through all refetch attempts to
|
|
205
|
+
hunt for the structured error that may be delayed by the write-order race.
|
|
206
|
+
|
|
207
|
+
Returns ``None`` when all attempts are exhausted without detecting any change.
|
|
208
|
+
"""
|
|
209
|
+
original_status = None
|
|
210
|
+
original_error = None
|
|
211
|
+
original_result = None
|
|
212
|
+
if original_data is not None:
|
|
213
|
+
original_status = str(original_data.get("status", "")).strip().lower()
|
|
214
|
+
original_error = original_data.get("error")
|
|
215
|
+
original_result = original_data.get("result")
|
|
216
|
+
|
|
217
|
+
for _ in range(_JOB_FAILED_REFETCH_ATTEMPTS):
|
|
218
|
+
await asyncio.sleep(_JOB_FAILED_REFETCH_SLEEP_SECONDS)
|
|
219
|
+
resp = await rpc.execute_command("queue_get_job_status", {"job_id": job_id})
|
|
220
|
+
data = resp.get("data") if isinstance(resp, dict) else None
|
|
221
|
+
if not isinstance(data, dict):
|
|
222
|
+
data = resp if isinstance(resp, dict) else {}
|
|
223
|
+
|
|
224
|
+
# Detect any of the state changes we were hunting for:
|
|
225
|
+
# Compare against original snapshot if provided.
|
|
226
|
+
|
|
227
|
+
# - Status changed
|
|
228
|
+
status = str(data.get("status", "")).strip().lower()
|
|
229
|
+
if original_status is not None and status != original_status:
|
|
230
|
+
if status in _FAILED_JOB_STATUSES:
|
|
231
|
+
return data
|
|
232
|
+
# When no original is provided, accept any failed status change
|
|
233
|
+
elif original_status is None and status in _FAILED_JOB_STATUSES:
|
|
234
|
+
return data
|
|
235
|
+
|
|
236
|
+
# - Result payload appeared or changed
|
|
237
|
+
current_result = data.get("result")
|
|
238
|
+
if original_result != current_result:
|
|
239
|
+
if isinstance(current_result, dict) and current_result:
|
|
240
|
+
return data
|
|
241
|
+
|
|
242
|
+
# - Inner error appeared or changed (nested structured error)
|
|
243
|
+
current_inner_error = _extract_inner_command_error(data)
|
|
244
|
+
original_inner_error = (
|
|
245
|
+
_extract_inner_command_error(original_data) if original_data else None
|
|
246
|
+
)
|
|
247
|
+
if current_inner_error != original_inner_error and current_inner_error is not None:
|
|
248
|
+
return data
|
|
249
|
+
|
|
250
|
+
# - Top-level error appeared or changed
|
|
251
|
+
current_error = data.get("error")
|
|
252
|
+
if current_error != original_error and current_error is not None:
|
|
253
|
+
return data
|
|
254
|
+
|
|
255
|
+
return None
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
async def _refetch_inner_command_error(rpc: Any, job_id: str, original_data: Optional[Dict[str, Any]] = None) -> Any:
|
|
183
259
|
"""Re-poll ``queue_get_job_status`` hunting for the inner structured error.
|
|
184
260
|
|
|
185
261
|
Write-order gotcha: the queue store writes the job's terminal ``status``
|
|
@@ -190,15 +266,9 @@ async def _refetch_inner_command_error(rpc: Any, job_id: str) -> Any:
|
|
|
190
266
|
the store. Re-fetching a few times with a short sleep gives that second
|
|
191
267
|
write time to land instead of silently downgrading to ``None``.
|
|
192
268
|
"""
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
data = resp.get("data") if isinstance(resp, dict) else None
|
|
197
|
-
if not isinstance(data, dict):
|
|
198
|
-
data = resp if isinstance(resp, dict) else {}
|
|
199
|
-
inner_error = _extract_inner_command_error(data)
|
|
200
|
-
if inner_error is not None:
|
|
201
|
-
return inner_error
|
|
269
|
+
data = await _refetch_job_status_with_payload(rpc, job_id, original_data=original_data)
|
|
270
|
+
if data is not None:
|
|
271
|
+
return _extract_inner_command_error(data)
|
|
202
272
|
return None
|
|
203
273
|
|
|
204
274
|
|
|
@@ -219,8 +289,8 @@ async def unwrap_job_result(
|
|
|
219
289
|
bare queue-level ``error`` field. When that nested error is not yet
|
|
220
290
|
present in ``status_data`` and ``rpc`` was supplied, it re-fetches the job
|
|
221
291
|
status up to :data:`_JOB_FAILED_REFETCH_ATTEMPTS` times (see
|
|
222
|
-
:func:`
|
|
223
|
-
out). The queue-level ``error`` field is used as a fallback when no
|
|
292
|
+
:func:`_refetch_job_status_with_payload` for the write-order race this
|
|
293
|
+
rides out). The queue-level ``error`` field is used as a fallback when no
|
|
224
294
|
structured inner error is ever found; ``JobFailedError.error`` is ``None``
|
|
225
295
|
only when genuinely nothing was found either way.
|
|
226
296
|
"""
|
|
@@ -231,7 +301,7 @@ async def unwrap_job_result(
|
|
|
231
301
|
if status in _FAILED_JOB_STATUSES or error:
|
|
232
302
|
inner_error = _extract_inner_command_error(status_data)
|
|
233
303
|
if inner_error is None and rpc is not None and job_id:
|
|
234
|
-
inner_error = await _refetch_inner_command_error(rpc, str(job_id))
|
|
304
|
+
inner_error = await _refetch_inner_command_error(rpc, str(job_id), original_data=status_data)
|
|
235
305
|
raise JobFailedError(
|
|
236
306
|
job_id, inner_error if inner_error is not None else error, status=status
|
|
237
307
|
)
|
|
@@ -242,6 +312,24 @@ async def unwrap_job_result(
|
|
|
242
312
|
else:
|
|
243
313
|
inner = result_field
|
|
244
314
|
|
|
315
|
+
# Handle the write-order race on the completed-status path: when
|
|
316
|
+
# status="completed" is observed but result is missing or empty {},
|
|
317
|
+
# the status might later transition to "failed" with the structured error.
|
|
318
|
+
# Refetch up to the configured attempt limit, checking for either:
|
|
319
|
+
# - Status transitioned to failed/stopped/cancelled
|
|
320
|
+
# - Result payload appeared or became populated
|
|
321
|
+
# If we detect any state change, process accordingly (raise if failed,
|
|
322
|
+
# return if populated). If stable empty result after all attempts, continue.
|
|
323
|
+
if status == "completed":
|
|
324
|
+
result_looks_empty = not result_field or (
|
|
325
|
+
isinstance(result_field, dict) and not result_field
|
|
326
|
+
)
|
|
327
|
+
if result_looks_empty and rpc is not None and job_id:
|
|
328
|
+
refetch_data = await _refetch_job_status_with_payload(rpc, str(job_id), original_data=status_data)
|
|
329
|
+
if refetch_data is not None:
|
|
330
|
+
# Status or payload changed; re-process this new snapshot recursively.
|
|
331
|
+
return await unwrap_job_result(refetch_data, rpc=rpc)
|
|
332
|
+
|
|
245
333
|
inner_looks_failed = isinstance(inner, dict) and inner.get("success") is False
|
|
246
334
|
command_success_false = status_data.get("command_success") is False
|
|
247
335
|
completed_with_error = bool(status_data.get("completed_with_error")) or bool(
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.6.151
|
{code_analysis_client-1.6.148 → code_analysis_client-1.6.151}/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.151
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
1.6.148
|
|
File without changes
|
{code_analysis_client-1.6.148 → code_analysis_client-1.6.151}/code_analysis_client/__init__.py
RENAMED
|
File without changes
|
{code_analysis_client-1.6.148 → code_analysis_client-1.6.151}/code_analysis_client/client.py
RENAMED
|
File without changes
|
{code_analysis_client-1.6.148 → code_analysis_client-1.6.151}/code_analysis_client/commands_proxy.py
RENAMED
|
File without changes
|
{code_analysis_client-1.6.148 → code_analysis_client-1.6.151}/code_analysis_client/config.py
RENAMED
|
File without changes
|
{code_analysis_client-1.6.148 → code_analysis_client-1.6.151}/code_analysis_client/exceptions.py
RENAMED
|
File without changes
|
{code_analysis_client-1.6.148 → code_analysis_client-1.6.151}/code_analysis_client/file_session.py
RENAMED
|
File without changes
|
|
File without changes
|
{code_analysis_client-1.6.148 → code_analysis_client-1.6.151}/code_analysis_client/responses.py
RENAMED
|
File without changes
|
|
File without changes
|
{code_analysis_client-1.6.148 → code_analysis_client-1.6.151}/code_analysis_client/server_api.py
RENAMED
|
File without changes
|
{code_analysis_client-1.6.148 → code_analysis_client-1.6.151}/code_analysis_client/server_schema.py
RENAMED
|
File without changes
|
{code_analysis_client-1.6.148 → code_analysis_client-1.6.151}/code_analysis_client/universal_file.py
RENAMED
|
File without changes
|
{code_analysis_client-1.6.148 → code_analysis_client-1.6.151}/code_analysis_client/validation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|