Source code for tooluniverse.planteome_tool
# planteome_tool.py
"""
Planteome tools for ToolUniverse -- plant ontologies and gene annotations.
Planteome (planteome.org), a Global Core Biodata Resource with no prior
ToolUniverse coverage, hosts the Plant Ontology, Plant Trait Ontology,
Plant Stress Ontology, and crop-specific ontologies (e.g. banana,
soybean), plus a GOlr-style search index of gene-to-ontology-term
annotations across plant species. This is distinct from ToolUniverse's
generic GO tools (OLS/QuickGO): Planteome indexes plant-specific trait
and structure ontologies those don't carry, and its annotation search
resolves a specific plant gene (e.g. an Arabidopsis AGI locus code) to
every ontology term it has been annotated with, across GO, PO, TO, and
crop ontologies together.
API: https://browser.planteome.org/api (documented at
planteome.org/web_services)
No authentication required.
"""
from typing import Any, Dict, List
import requests
from .base_tool import BaseTool
from .tool_registry import register_tool
PLANTEOME_API_URL = "https://browser.planteome.org/api"
_TERM_FIELDS = (
"id",
"annotation_class_label",
"description",
"source",
"is_obsolete",
"synonym",
)
_ANNOTATION_FIELDS = (
"bioentity",
"bioentity_label",
"annotation_class",
"annotation_class_label",
"aspect",
"taxon_label",
"evidence_type",
"reference",
)
def _planteome_get(path: str, params: Dict[str, Any], timeout: int):
"""GET a Planteome endpoint, returning (payload, error_envelope)."""
try:
resp = requests.get(f"{PLANTEOME_API_URL}{path}", params=params, timeout=timeout)
except requests.exceptions.Timeout:
return None, {
"status": "error",
"error": f"Planteome request timed out after {timeout}s",
}
except requests.exceptions.RequestException as e:
return None, {"status": "error", "error": f"Planteome request failed: {e}"}
resp.raise_for_status()
try:
payload = resp.json()
except ValueError:
return None, {"status": "error", "error": "Planteome returned a non-JSON response."}
return payload, None
def _summarize(hit: Dict[str, Any], fields) -> Dict[str, Any]:
return {f: hit.get(f) for f in fields}
[docs]
@register_tool("PlanteomeTool")
class PlanteomeTool(BaseTool):
"""
Tool for querying Planteome, dispatched by fields.operation:
- "search_terms" : keyword search over plant ontology terms
- "get_term" : a single ontology term by its accession ID
- "search_annotations" : gene/bioentity to ontology-term annotations
No authentication required.
"""
[docs]
def __init__(self, tool_config: Dict[str, Any]):
super().__init__(tool_config)
self.timeout = tool_config.get("timeout", 30)
self.operation = tool_config.get("fields", {}).get("operation", "search_terms")
[docs]
def run(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
if self.operation == "search_terms":
return self._search_terms(arguments)
if self.operation == "get_term":
return self._get_term(arguments)
if self.operation == "search_annotations":
return self._search_annotations(arguments)
return {"status": "error", "error": f"Unknown operation: {self.operation}"}
[docs]
def _limit(self, arguments: Dict[str, Any], default: int = 20) -> int:
try:
return max(1, min(int(arguments.get("limit") or default), 100))
except (TypeError, ValueError):
return default
[docs]
def _search_terms(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
query = (arguments.get("query") or "").strip()
if not query:
return {
"status": "error",
"error": "query is required, e.g. 'pollen development'.",
}
payload, err = _planteome_get(
"/search/ontology", {"q": query}, self.timeout
)
if err is not None:
return err
hits = payload.get("data") or []
limit = self._limit(arguments)
return {
"status": "success",
"data": [_summarize(h, _TERM_FIELDS) for h in hits[:limit]],
"metadata": {
"query": query,
"returned": min(len(hits), limit),
"source": "Planteome (planteome.org)",
},
}
[docs]
def _get_term(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
term_id = (arguments.get("term_id") or "").strip()
if not term_id:
return {
"status": "error",
"error": "term_id is required, e.g. 'GO:0009555' or 'PO:0025281'.",
}
payload, err = _planteome_get("/entity/terms", {"entity": term_id}, self.timeout)
if err is not None:
return err
if payload.get("status") != "success" or not payload.get("data"):
return {
"status": "error",
"error": f"No Planteome ontology term found for '{term_id}'.",
}
hits = payload["data"] if isinstance(payload["data"], list) else list(payload["data"].values())
if not hits:
return {
"status": "error",
"error": f"No Planteome ontology term found for '{term_id}'.",
}
return {
"status": "success",
"data": _summarize(hits[0], _TERM_FIELDS),
"metadata": {"term_id": term_id, "source": "Planteome (planteome.org)"},
}
[docs]
def _search_annotations(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
query = (arguments.get("query") or "").strip()
if not query:
return {
"status": "error",
"error": "query is required, e.g. a gene id like 'AT4G32150'.",
}
payload, err = _planteome_get(
"/search/annotation", {"q": query}, self.timeout
)
if err is not None:
return err
hits = payload.get("data") or []
limit = self._limit(arguments)
return {
"status": "success",
"data": [_summarize(h, _ANNOTATION_FIELDS) for h in hits[:limit]],
"metadata": {
"query": query,
"returned": min(len(hits), limit),
"source": "Planteome (planteome.org)",
},
}