Source code for tooluniverse.emdb_tool

import requests
from typing import Any, Dict, Tuple
from .base_tool import BaseTool
from .http_utils import request_with_retry
from .tool_registry import register_tool


[docs] @register_tool("EMDBRESTTool") class EMDBRESTTool(BaseTool):
[docs] def __init__(self, tool_config: Dict): super().__init__(tool_config) self.base_url = "https://www.ebi.ac.uk/emdb/api" self.session = requests.Session() self.session.headers.update({"Accept": "application/json"}) self.timeout = 30
[docs] def _build_url(self, args: Dict[str, Any]) -> Tuple[str, Dict[str, Any]]: """Substitute `{placeholder}` tokens in the endpoint template. Fix-R18A-1: args with no matching `{placeholder}` (e.g. EMDB_search_structures's `rows`) were silently dropped -- there was no query-string fallback, so `rows` had zero effect on the request regardless of value (confirmed live: rows=2 and rows=10 both returned the same 10 results; the raw EMDB API honors `?rows=N` as a query param). Return them separately so the caller can pass them through as query params instead. """ url = self.tool_config["fields"]["endpoint"] query_params = dict(self.tool_config.get("fields", {}).get("params", {})) param_mapping = self.tool_config.get("fields", {}).get("param_mapping", {}) for k, v in args.items(): token = f"{{{k}}}" if token in url: url = url.replace(token, str(v)) else: query_params[param_mapping.get(k, k)] = v return url, query_params
[docs] def run(self, arguments: Dict[str, Any]) -> Dict[str, Any]: url = None try: if ( self.tool_config.get("fields", {}).get("pagination") == "ebi_search_start" ): rows = arguments.get("rows", 10) start = arguments.get("start", 0) if ( isinstance(rows, bool) or not isinstance(rows, int) or not 1 <= rows <= 100 ): return { "status": "error", "error": "rows must be an integer between 1 and 100", "parameter": "rows", } if isinstance(start, bool) or not isinstance(start, int) or start < 0: return { "status": "error", "error": "start must be a non-negative integer", "parameter": "start", } arguments = dict(arguments) arguments["rows"] = rows arguments["start"] = start url, query_params = self._build_url(arguments) response = request_with_retry( self.session, "GET", url, params=query_params, timeout=self.timeout, max_attempts=3, ) if response.status_code != 200: return { "status": "error", "error": "EMDB API error", "url": url, "status_code": response.status_code, "detail": (response.text or "")[:500], } data = response.json() result = {"status": "success", "data": data, "url": url} if ( self.tool_config.get("fields", {}).get("pagination") == "ebi_search_start" ): entries = data.get("entries", []) if isinstance(data, dict) else [] if not isinstance(entries, list): entries = [] total = data.get("hitCount") if isinstance(data, dict) else None total_is_exact = isinstance(total, int) and not isinstance(total, bool) returned = len(entries) start = arguments["start"] rows = arguments["rows"] has_more = bool( total_is_exact and returned and start + returned < total ) stalled = bool(total_is_exact and not returned and start < total) result["metadata"] = { "returned": returned, "total_results": total if total_is_exact else None, "total_results_is_exact": total_is_exact, "start": start, "rows": rows, "has_more": has_more, "next_start": start + returned if has_more else None, "truncated": start > 0 or has_more, "result_set_complete": bool( total_is_exact and start == 0 and returned == total ), "pagination_stalled": stalled, "pagination_scope": "ebi_search_emdb_start_hit_count", "warning": ( "EBI Search reported additional EMDB entries but returned an empty page at this start." if stalled else None ), } return result except Exception as e: return { "status": "error", "error": f"EMDB API error: {str(e)}", "url": url, }