Source code for tooluniverse.biomodels_tool

from typing import Dict, Any, List, Optional
import requests
from .base_tool import BaseTool
from .base_rest_tool import BaseRESTTool
from .tool_registry import register_tool


[docs] @register_tool("BioModelsRESTTool") class BioModelsRESTTool(BaseRESTTool): """Generic REST tool for BioModels API endpoints."""
[docs] def _get_param_mapping(self) -> Dict[str, str]: """Map BioModels-specific parameter names.""" if "parameterSearch" in self.tool_config.get("fields", {}).get("endpoint", ""): return { "limit": "size", "offset": "start", } return { "limit": "numResults", # limit -> numResults "offset": "offset", # query and filename use their original names }
[docs] def _build_params(self, args: Dict[str, Any]) -> Dict[str, Any]: """Build params while respecting BioModels' minimum upstream page size of 10.""" params = super()._build_params(args) if self.tool_config.get("fields", {}).get("pagination") == "biomodels_offset": size_key = "size" if "parameterSearch" in self.tool_config["fields"]["endpoint"] else "numResults" if isinstance(params.get(size_key), int) and params[size_key] < 10: params[size_key] = 10 return params
[docs] def run(self, arguments: Dict[str, Any]) -> Dict[str, Any]: """Validate recoverable offset pagination before invoking BaseRESTTool.""" if self.tool_config.get("fields", {}).get("pagination") == "biomodels_offset": props = self.tool_config.get("parameter", {}).get("properties", {}) limit = arguments.get("limit", props.get("limit", {}).get("default", 10)) offset = arguments.get("offset", props.get("offset", {}).get("default", 0)) if isinstance(limit, bool) or not isinstance(limit, int) or not 1 <= limit <= 100: return {"status": "error", "error": "limit must be an integer from 1 to 100"} if isinstance(offset, bool) or not isinstance(offset, int) or offset < 0: return {"status": "error", "error": "offset must be a non-negative integer"} arguments = dict(arguments, limit=limit, offset=offset) return super().run(arguments)
[docs] def _handle_special_endpoint( self, url: str, response: requests.Response, arguments: Dict[str, Any] ) -> Optional[Dict[str, Any]]: """Handle download endpoints specially.""" if self.tool_config.get("fields", {}).get("pagination") == "biomodels_offset": try: data = response.json() except ValueError: return None parameter_search = "parameterSearch" in url records_key = "entries" if parameter_search else "models" total_key = "recordsFiltered" if parameter_search else "matches" records = data.get(records_key, []) if isinstance(data, dict) else [] if not isinstance(records, list): records = [] limit = arguments["limit"] offset = arguments["offset"] page = records[:limit] total = data.get(total_key) if isinstance(data, dict) else None total_is_exact = isinstance(total, int) and not isinstance(total, bool) next_offset = offset + len(page) stalled = bool(total_is_exact and not page and offset < total) has_more = bool( total_is_exact and page and next_offset < total ) normalized = dict(data) normalized[records_key] = page return { "status": "success", "data": normalized, "count": len(page), "url": url, "metadata": { "returned": len(page), "total_results": total if total_is_exact else None, "total_results_is_exact": total_is_exact, "offset": offset, "limit": limit, "has_more": has_more, "next_offset": next_offset if has_more else None, "truncated": offset > 0 or has_more, "result_set_complete": bool( total_is_exact and offset == 0 and len(page) == total ), "pagination_stalled": stalled, "pagination_scope": ( "biomodels_parameter_size_start" if parameter_search else "biomodels_model_numresults_offset" ), "warning": ( "BioModels reported additional matches but returned an empty page at this offset." if stalled else None ), }, } if "download" in url: content_disposition = response.headers.get("Content-Disposition", "") content_type = response.headers.get("Content-Type", "") # Wrap in data field for schema validation return { "status": "success", "data": { "download_url": url, "filename": content_disposition, "content_type": content_type, }, "url": url, } return None
[docs] def _process_response( self, response: requests.Response, url: str ) -> Dict[str, Any]: """Process BioModels API response.""" try: data = response.json() except ValueError: text = response.text or "" content_type = response.headers.get("Content-Type", "") retryable = "text/html" in content_type.lower() or text.lstrip().startswith("<") return { "status": "error", "error": "BioModels returned a non-JSON response", "url": url, "content_type": content_type, "response_snippet": text[:200], "retryable": retryable, "suggestion": ( "BioModels may have returned an HTML maintenance or redirect page. " "Check the endpoint URL, request parameters, and service availability." ), } # Build result result = { "status": "success", "data": data, "url": url, } # Add count for lists or search results if isinstance(data, list): result["count"] = len(data) elif isinstance(data, dict) and "matches" in data: # matches is an integer count, not a list result["count"] = data.get("matches", 0) return result