Source code for tooluniverse.disprot_tool
# disprot_tool.py
"""
DisProt tool for ToolUniverse.
DisProt is a manually curated database of intrinsically disordered proteins
and regions, providing experimentally validated disorder annotations with
evidence codes and literature references.
API: https://disprot.org/api/
No authentication required.
"""
import re
import requests
from typing import Dict, Any, List
from .base_tool import BaseTool
from .tool_registry import register_tool
DISPROT_BASE_URL = "https://www.disprot.org/api"
_DISPROT_ID = re.compile(r"^DP\d{5}$", re.I)
_UNIPROT_ACC = re.compile(
r"^(?:[OPQ][0-9][A-Z0-9]{3}[0-9]|[A-NR-Z][0-9](?:[A-Z][A-Z0-9]{2}[0-9]){1,2})$",
re.I,
)
[docs]
@register_tool("DisProtTool")
class DisProtTool(BaseTool):
"""
Tool for querying DisProt intrinsically disordered protein database.
Supports:
- Search disordered proteins by text query
- Get detailed disorder region annotations for a specific protein
No authentication required.
"""
[docs]
def __init__(self, tool_config: Dict[str, Any]):
super().__init__(tool_config)
self.timeout = tool_config.get("timeout", 60)
fields = tool_config.get("fields", {})
self.endpoint = fields.get("endpoint", "search")
[docs]
def run(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
"""Execute the DisProt API call."""
try:
return self._query(arguments)
except requests.exceptions.Timeout:
return {
"status": "error",
"error": f"DisProt API timed out after {self.timeout}s.",
}
except requests.exceptions.ConnectionError:
return {
"status": "error",
"error": "Failed to connect to DisProt API (www.disprot.org).",
}
except requests.exceptions.HTTPError as e:
status = e.response.status_code if e.response is not None else "unknown"
if status == 404:
return {
"status": "error",
"error": "Entry not found in DisProt. Check the accession.",
}
return {"status": "error", "error": f"DisProt API HTTP {status}"}
except Exception as e:
return {"status": "error", "error": f"Unexpected error: {str(e)}"}
[docs]
def _query(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
"""Route to appropriate endpoint."""
if self.endpoint == "search":
return self._search(arguments)
elif self.endpoint == "get_entry":
return self._get_entry(arguments)
else:
return {"status": "error", "error": f"Unknown endpoint: {self.endpoint}"}
[docs]
def _search_fields(self, query: str) -> List[str]:
"""Which DisProt filters to try for this query, in order.
DisProt's /search ignores a free-text ``q`` and returns every entry, so the
query has to be sent as a real filter: an exact identifier when it looks
like one, otherwise the gene and protein-name filters (substring,
case-insensitive), then the organism filter.
"""
if _DISPROT_ID.match(query):
return ["disprot_id"]
if _UNIPROT_ACC.match(query):
return ["acc"]
return ["gene", "name", "organism"]
[docs]
def _search(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
"""Search DisProt for disordered proteins."""
query = (arguments.get("query") or "").strip()
if not query:
return {
"status": "error",
"error": "query is required (e.g., 'TP53', 'kinase', 'amyloid').",
}
page_size = min(arguments.get("page_size") or 10, 20)
url = f"{DISPROT_BASE_URL}/search"
entries: List[Dict[str, Any]] = []
seen = set()
matches: Dict[str, int] = {}
for field in self._search_fields(query):
# The organism filter is only a fallback for queries that name no
# gene or protein; it would otherwise flood the list with a proteome.
if field == "organism" and entries:
break
value = query.upper() if field in ("disprot_id", "acc") else query
params = {field: value, "release": "current", "page_size": page_size}
response = requests.get(url, params=params, timeout=self.timeout)
response.raise_for_status()
data = response.json()
matches[field] = data.get("size", len(data.get("data", [])))
for entry in data.get("data", []):
key = entry.get("acc") or entry.get("disprot_id")
if key in seen:
continue
seen.add(key)
genes = entry.get("genes", [])
gene_name = genes[0].get("name", {}).get("value", "") if genes else ""
entries.append(
{
"disprot_id": entry.get("disprot_id"),
"acc": entry.get("acc"),
"name": entry.get("name"),
"gene": gene_name,
"organism": entry.get("organism"),
"length": entry.get("length"),
"disorder_content": entry.get("disorder_content"),
"regions_counter": entry.get("regions_counter"),
"dataset": entry.get("dataset", []),
"matched_on": field,
}
)
return {
"status": "success",
"data": entries[:page_size],
"metadata": {
"source": "DisProt (disprot.org)",
"query": query,
"returned": len(entries[:page_size]),
"total_matches_by_filter": matches,
},
}
[docs]
def _get_entry(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
"""Get detailed disorder regions for a DisProt entry."""
accession = arguments.get("accession", "")
if not accession:
return {
"status": "error",
"error": "accession is required. Use DisProt ID (e.g., 'DP00086') or UniProt accession (e.g., 'P04637').",
}
# Determine whether it's a DisProt ID (DP*) or UniProt accession
url = f"{DISPROT_BASE_URL}/search"
if accession.upper().startswith("DP"):
params = {"disprot_id": accession.upper(), "page_size": 1}
else:
params = {"acc": accession, "page_size": 1}
response = requests.get(url, params=params, timeout=self.timeout)
response.raise_for_status()
search_result = response.json()
results = search_result.get("data", [])
if not results:
return {
"status": "error",
"error": f"Entry not found in DisProt for accession '{accession}'.",
}
data = results[0]
# Extract regions
regions = []
for region in data.get("regions", []):
cross_refs = []
for xr in region.get("cross_refs", []):
cross_refs.append(
{
"db": xr.get("db"),
"id": xr.get("id"),
}
)
regions.append(
{
"region_id": region.get("region_id"),
"start": region.get("start"),
"end": region.get("end"),
"term_name": region.get("term_name"),
"term_namespace": region.get("term_namespace"),
"evidence_code": region.get("ec_name"),
"method": region.get("method_name"),
"cross_refs": cross_refs[:5], # Limit refs
}
)
genes = data.get("genes", [])
gene_name = genes[0].get("name", {}).get("value", "") if genes else ""
return {
"status": "success",
"data": {
"disprot_id": data.get("disprot_id"),
"acc": data.get("acc"),
"name": data.get("name"),
"gene": gene_name,
"organism": data.get("organism"),
"length": data.get("length"),
"disorder_content": data.get("disorder_content"),
"regions_counter": data.get("regions_counter"),
"dataset": data.get("dataset", []),
"regions": regions[:30], # Limit to 30 regions
},
"metadata": {
"source": "DisProt (disprot.org)",
"total_regions": data.get("regions_counter", len(regions)),
},
}