code-analysis-client 1.6.148__tar.gz → 1.6.150__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.
Files changed (25) hide show
  1. {code_analysis_client-1.6.148 → code_analysis_client-1.6.150}/PKG-INFO +1 -1
  2. {code_analysis_client-1.6.148 → code_analysis_client-1.6.150}/code_analysis_client/queue_wait.py +102 -14
  3. code_analysis_client-1.6.150/code_analysis_client/version.txt +1 -0
  4. {code_analysis_client-1.6.148 → code_analysis_client-1.6.150}/code_analysis_client.egg-info/PKG-INFO +1 -1
  5. code_analysis_client-1.6.148/code_analysis_client/version.txt +0 -1
  6. {code_analysis_client-1.6.148 → code_analysis_client-1.6.150}/README.md +0 -0
  7. {code_analysis_client-1.6.148 → code_analysis_client-1.6.150}/code_analysis_client/__init__.py +0 -0
  8. {code_analysis_client-1.6.148 → code_analysis_client-1.6.150}/code_analysis_client/client.py +0 -0
  9. {code_analysis_client-1.6.148 → code_analysis_client-1.6.150}/code_analysis_client/commands_proxy.py +0 -0
  10. {code_analysis_client-1.6.148 → code_analysis_client-1.6.150}/code_analysis_client/config.py +0 -0
  11. {code_analysis_client-1.6.148 → code_analysis_client-1.6.150}/code_analysis_client/exceptions.py +0 -0
  12. {code_analysis_client-1.6.148 → code_analysis_client-1.6.150}/code_analysis_client/file_session.py +0 -0
  13. {code_analysis_client-1.6.148 → code_analysis_client-1.6.150}/code_analysis_client/py.typed +0 -0
  14. {code_analysis_client-1.6.148 → code_analysis_client-1.6.150}/code_analysis_client/responses.py +0 -0
  15. {code_analysis_client-1.6.148 → code_analysis_client-1.6.150}/code_analysis_client/schema_disk_cache.py +0 -0
  16. {code_analysis_client-1.6.148 → code_analysis_client-1.6.150}/code_analysis_client/server_api.py +0 -0
  17. {code_analysis_client-1.6.148 → code_analysis_client-1.6.150}/code_analysis_client/server_schema.py +0 -0
  18. {code_analysis_client-1.6.148 → code_analysis_client-1.6.150}/code_analysis_client/universal_file.py +0 -0
  19. {code_analysis_client-1.6.148 → code_analysis_client-1.6.150}/code_analysis_client/validation.py +0 -0
  20. {code_analysis_client-1.6.148 → code_analysis_client-1.6.150}/code_analysis_client.egg-info/SOURCES.txt +0 -0
  21. {code_analysis_client-1.6.148 → code_analysis_client-1.6.150}/code_analysis_client.egg-info/dependency_links.txt +0 -0
  22. {code_analysis_client-1.6.148 → code_analysis_client-1.6.150}/code_analysis_client.egg-info/requires.txt +0 -0
  23. {code_analysis_client-1.6.148 → code_analysis_client-1.6.150}/code_analysis_client.egg-info/top_level.txt +0 -0
  24. {code_analysis_client-1.6.148 → code_analysis_client-1.6.150}/pyproject.toml +0 -0
  25. {code_analysis_client-1.6.148 → code_analysis_client-1.6.150}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-analysis-client
3
- Version: 1.6.148
3
+ Version: 1.6.150
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
@@ -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 = 4
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 _refetch_inner_command_error(rpc: Any, job_id: str) -> Any:
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
- for _ in range(_JOB_FAILED_REFETCH_ATTEMPTS):
194
- await asyncio.sleep(_JOB_FAILED_REFETCH_SLEEP_SECONDS)
195
- resp = await rpc.execute_command("queue_get_job_status", {"job_id": job_id})
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:`_refetch_inner_command_error` for the write-order race this rides
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(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-analysis-client
3
- Version: 1.6.148
3
+ Version: 1.6.150
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