code-analysis-client 1.6.57__tar.gz → 1.6.58__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 (24) hide show
  1. {code_analysis_client-1.6.57 → code_analysis_client-1.6.58}/PKG-INFO +1 -1
  2. {code_analysis_client-1.6.57 → code_analysis_client-1.6.58}/code_analysis_client/client.py +1 -1
  3. {code_analysis_client-1.6.57 → code_analysis_client-1.6.58}/code_analysis_client/queue_wait.py +66 -2
  4. code_analysis_client-1.6.58/code_analysis_client/version.txt +1 -0
  5. {code_analysis_client-1.6.57 → code_analysis_client-1.6.58}/code_analysis_client.egg-info/PKG-INFO +1 -1
  6. code_analysis_client-1.6.57/code_analysis_client/version.txt +0 -1
  7. {code_analysis_client-1.6.57 → code_analysis_client-1.6.58}/README.md +0 -0
  8. {code_analysis_client-1.6.57 → code_analysis_client-1.6.58}/code_analysis_client/__init__.py +0 -0
  9. {code_analysis_client-1.6.57 → code_analysis_client-1.6.58}/code_analysis_client/commands_proxy.py +0 -0
  10. {code_analysis_client-1.6.57 → code_analysis_client-1.6.58}/code_analysis_client/config.py +0 -0
  11. {code_analysis_client-1.6.57 → code_analysis_client-1.6.58}/code_analysis_client/exceptions.py +0 -0
  12. {code_analysis_client-1.6.57 → code_analysis_client-1.6.58}/code_analysis_client/file_session.py +0 -0
  13. {code_analysis_client-1.6.57 → code_analysis_client-1.6.58}/code_analysis_client/py.typed +0 -0
  14. {code_analysis_client-1.6.57 → code_analysis_client-1.6.58}/code_analysis_client/responses.py +0 -0
  15. {code_analysis_client-1.6.57 → code_analysis_client-1.6.58}/code_analysis_client/server_api.py +0 -0
  16. {code_analysis_client-1.6.57 → code_analysis_client-1.6.58}/code_analysis_client/server_schema.py +0 -0
  17. {code_analysis_client-1.6.57 → code_analysis_client-1.6.58}/code_analysis_client/universal_file.py +0 -0
  18. {code_analysis_client-1.6.57 → code_analysis_client-1.6.58}/code_analysis_client/validation.py +0 -0
  19. {code_analysis_client-1.6.57 → code_analysis_client-1.6.58}/code_analysis_client.egg-info/SOURCES.txt +0 -0
  20. {code_analysis_client-1.6.57 → code_analysis_client-1.6.58}/code_analysis_client.egg-info/dependency_links.txt +0 -0
  21. {code_analysis_client-1.6.57 → code_analysis_client-1.6.58}/code_analysis_client.egg-info/requires.txt +0 -0
  22. {code_analysis_client-1.6.57 → code_analysis_client-1.6.58}/code_analysis_client.egg-info/top_level.txt +0 -0
  23. {code_analysis_client-1.6.57 → code_analysis_client-1.6.58}/pyproject.toml +0 -0
  24. {code_analysis_client-1.6.57 → code_analysis_client-1.6.58}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-analysis-client
3
- Version: 1.6.57
3
+ Version: 1.6.58
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
@@ -184,7 +184,7 @@ class CodeAnalysisAsyncClient:
184
184
  poll_interval=poll_interval,
185
185
  status_hook=status_hook,
186
186
  )
187
- return unwrap_job_result(status)
187
+ return await unwrap_job_result(status, rpc=self._rpc)
188
188
 
189
189
  async def call_validated(
190
190
  self,
@@ -155,7 +155,55 @@ async def wait_for_job(
155
155
  await asyncio.sleep(poll_interval)
156
156
 
157
157
 
158
- def unwrap_job_result(status_data: Dict[str, Any]) -> Dict[str, Any]:
158
+ _JOB_FAILED_REFETCH_ATTEMPTS = 4
159
+ _JOB_FAILED_REFETCH_SLEEP_SECONDS = 0.5
160
+
161
+
162
+ def _extract_inner_command_error(status_data: Dict[str, Any]) -> Any:
163
+ """Pull the failed command's own structured error out of a job status dict.
164
+
165
+ Looks at ``status_data["result"]["result"]["error"]`` first (outer
166
+ ``result`` is the queue wrapper, inner ``result`` is the command's own
167
+ result envelope), falling back to ``status_data["result"]["error"]``.
168
+ Returns ``None`` when neither is present.
169
+ """
170
+ result_field = status_data.get("result")
171
+ if not isinstance(result_field, dict):
172
+ return None
173
+ inner_result = result_field.get("result")
174
+ if isinstance(inner_result, dict) and inner_result.get("error") is not None:
175
+ return inner_result["error"]
176
+ if result_field.get("error") is not None:
177
+ return result_field["error"]
178
+ return None
179
+
180
+
181
+ async def _refetch_inner_command_error(rpc: Any, job_id: str) -> Any:
182
+ """Re-poll ``queue_get_job_status`` hunting for the inner structured error.
183
+
184
+ Write-order gotcha: the queue store writes the job's terminal ``status``
185
+ slightly BEFORE it writes the ``result`` payload. A caller that reads the
186
+ status the instant it turns ``failed`` can therefore observe a terminal
187
+ status with no ``result`` (and no nested inner error) yet, even though the
188
+ failed command DID record a structured error - it just hasn't landed in
189
+ the store. Re-fetching a few times with a short sleep gives that second
190
+ write time to land instead of silently downgrading to ``None``.
191
+ """
192
+ for _ in range(_JOB_FAILED_REFETCH_ATTEMPTS):
193
+ await asyncio.sleep(_JOB_FAILED_REFETCH_SLEEP_SECONDS)
194
+ resp = await rpc.execute_command("queue_get_job_status", {"job_id": job_id})
195
+ data = resp.get("data") if isinstance(resp, dict) else None
196
+ if not isinstance(data, dict):
197
+ data = resp if isinstance(resp, dict) else {}
198
+ inner_error = _extract_inner_command_error(data)
199
+ if inner_error is not None:
200
+ return inner_error
201
+ return None
202
+
203
+
204
+ async def unwrap_job_result(
205
+ status_data: Dict[str, Any], *, rpc: Any = None
206
+ ) -> Dict[str, Any]:
159
207
  """Extract the inner command result from a terminal job status ``data`` dict.
160
208
 
161
209
  Raises :class:`JobFailedError` when the job itself failed/stopped/cancelled
@@ -163,13 +211,29 @@ def unwrap_job_result(status_data: Dict[str, Any]) -> Dict[str, Any]:
163
211
  completed but the inner command result is itself an error envelope.
164
212
  Otherwise returns the inner result dict, in the same shape a sync call
165
213
  would have returned.
214
+
215
+ On job failure, before raising, this tries to attach the failed command's
216
+ OWN structured error (nested at ``data.result.result.error``, falling
217
+ back to ``data.result.error``) to ``JobFailedError.error`` instead of the
218
+ bare queue-level ``error`` field. When that nested error is not yet
219
+ present in ``status_data`` and ``rpc`` was supplied, it re-fetches the job
220
+ status up to :data:`_JOB_FAILED_REFETCH_ATTEMPTS` times (see
221
+ :func:`_refetch_inner_command_error` for the write-order race this rides
222
+ out). The queue-level ``error`` field is used as a fallback when no
223
+ structured inner error is ever found; ``JobFailedError.error`` is ``None``
224
+ only when genuinely nothing was found either way.
166
225
  """
167
226
  job_id = status_data.get("job_id")
168
227
  status = str(status_data.get("status", "")).strip().lower()
169
228
  error = status_data.get("error")
170
229
 
171
230
  if status in _FAILED_JOB_STATUSES or error:
172
- raise JobFailedError(job_id, error, status=status)
231
+ inner_error = _extract_inner_command_error(status_data)
232
+ if inner_error is None and rpc is not None and job_id:
233
+ inner_error = await _refetch_inner_command_error(rpc, str(job_id))
234
+ raise JobFailedError(
235
+ job_id, inner_error if inner_error is not None else error, status=status
236
+ )
173
237
 
174
238
  result_field = status_data.get("result")
175
239
  if isinstance(result_field, dict) and "result" in result_field:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-analysis-client
3
- Version: 1.6.57
3
+ Version: 1.6.58
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