Source code for tooluniverse.base_rest_tool
"""
Base REST tool class with common functionality for API integrations.
This module provides a reusable base class for REST API tools that handles:
- URL building with path parameter substitution
- Query parameter construction
- HTTP requests with retry logic
- Standard error handling and response formatting
"""
import requests
import urllib.parse
from typing import Any, Dict, Optional, Callable
from .base_tool import BaseTool
from .http_utils import request_with_retry
[docs]
class BaseRESTTool(BaseTool):
"""
Base class for REST API tools with common HTTP request handling.
Provides reusable methods for:
- Building URLs with path parameters (e.g., {id}, {doi})
- Constructing query parameters
- Making HTTP requests with retry logic
- Standard error handling and response formatting
Subclasses should override:
- `_get_param_mapping()` - to customize parameter name mappings
- `_process_response()` - to customize response processing
- `_handle_special_endpoint()` - for endpoint-specific logic
"""
[docs]
def __init__(self, tool_config):
super().__init__(tool_config)
self.session = requests.Session()
self.timeout = 30
self.api_name = self.__class__.__name__.replace("RESTTool", "")
[docs]
def _get_param_mapping(self) -> Dict[str, str]:
"""
Get parameter name mappings from argument names to API parameter names.
Override this in subclasses to provide custom mappings.
Example: {"limit": "rows", "query": "q"}
"""
return {}
[docs]
def _build_url(self, args: Dict[str, Any]) -> str:
"""
Build URL by replacing path parameters like {id}, {doi}, {accession}.
Args:
args: Tool arguments dictionary
Returns:
Complete URL with path parameters substituted
"""
url = self.tool_config["fields"]["endpoint"]
# Replace all path parameters
for key, value in args.items():
placeholder = f"{{{key}}}"
if placeholder in url:
# URL encode to handle special characters (e.g., DOIs with slashes)
encoded_value = urllib.parse.quote(str(value), safe="")
url = url.replace(placeholder, encoded_value)
return url
[docs]
def _build_params(self, args: Dict[str, Any]) -> Dict[str, Any]:
"""
Build query parameters from arguments.
Args:
args: Tool arguments dictionary
Returns:
Query parameters dictionary
"""
params = {}
url_template = self.tool_config["fields"]["endpoint"]
# Add default params from config
default_params = self.tool_config.get("fields", {}).get("params", {})
params.update(default_params)
# Get param mapping for this API
param_mapping = self._get_param_mapping()
# Only add arguments that aren't path parameters
for key, value in args.items():
if f"{{{key}}}" not in url_template and value is not None:
# Use mapped parameter name if available
param_name = param_mapping.get(key, key)
params[param_name] = value
return params
[docs]
def _process_response(
self, response: requests.Response, url: str
) -> Dict[str, Any]:
"""
Process successful API response.
Override this in subclasses for API-specific response handling.
Args:
response: HTTP response object
url: Request URL
Returns:
Processed response dictionary
"""
data = response.json()
# Handle extract_path for nested data
extract_path = self.tool_config.get("fields", {}).get("extract_path")
if extract_path and isinstance(data, dict):
data = data.get(extract_path, data)
# Build result
result = {
"status": "success",
"data": data,
"url": url,
}
# Add count for lists
if isinstance(data, list):
result["count"] = len(data)
return result
[docs]
def _handle_special_endpoint(
self, url: str, response: requests.Response, arguments: Dict[str, Any]
) -> Optional[Dict[str, Any]]:
"""
Handle special endpoints that need custom processing.
Override this for endpoint-specific logic (e.g., download endpoints).
Return None to use default processing.
Args:
url: Request URL
response: HTTP response object
arguments: Original arguments
Returns:
Custom result dictionary or None for default processing
"""
return None
[docs]
def run(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
"""
Execute the API request.
Args:
arguments: Tool arguments dictionary
Returns:
Result dictionary with status, data, url, and optional error info
"""
url = None
try:
url = self._build_url(arguments)
params = self._build_params(arguments)
response = request_with_retry(
self.session,
"GET",
url,
params=params,
timeout=self.timeout,
max_attempts=3,
)
# Check for errors
if response.status_code != 200:
return {
"status": "error",
"error": f"{self.api_name} API error",
"url": url,
"status_code": response.status_code,
"detail": (response.text or "")[:500],
}
# Try special endpoint handling first
special_result = self._handle_special_endpoint(url, response, arguments)
if special_result is not None:
return special_result
# Use default response processing
return self._process_response(response, url)
except Exception as e:
return {
"status": "error",
"error": f"{self.api_name} API error: {str(e)}",
"url": url,
}