Source code for tooluniverse.lovd_tool

"""
LOVD (Leiden Open Variation Database) API tool for ToolUniverse.

LOVD is the world's largest open-source database system for DNA variants,
used by over 23,000 gene databases. The shared installation at
databases.lovd.nl/shared hosts curated variant data from gene-specific
databases including BRCA, TP53, and many other clinically relevant genes.

API: https://databases.lovd.nl/shared/api/rest.php/
Format: Atom XML by default, JSON with ?format=application/json
No authentication required.
"""

from typing import Any, Dict
from urllib.parse import quote

import requests

from .base_tool import BaseTool
from .tool_registry import register_tool

LOVD_BASE_URL = "https://databases.lovd.nl/shared/api/rest.php"


[docs] @register_tool("LOVDTool") class LOVDTool(BaseTool): """ Tool for querying LOVD (Leiden Open Variation Database) REST API. LOVD provides curated variant data including: - Gene information (symbol, chromosomal location, RefSeq, curators) - Variant details (DNA/RNA/protein notation, genomic position) - Variant search by DBID or HGVS DNA notation 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", "get_gene")
[docs] def run(self, arguments: Dict[str, Any]) -> Dict[str, Any]: """Execute the LOVD API call.""" try: operation = self.operation if operation == "get_gene": return self._get_gene(arguments) elif operation == "get_variants": return self._get_variants(arguments) elif operation == "search_variants": return self._search_variants(arguments) else: return {"status": "error", "error": f"Unknown operation: {operation}"} except ValueError as exc: return {"status": "error", "error": f"LOVD returned invalid JSON: {exc}"}
[docs] def _make_request(self, url: str, params: dict = None) -> requests.Response: """Make a GET request with JSON format parameter.""" if params is None: params = {} params["format"] = "application/json" response = requests.get(url, params=params, timeout=self.timeout) return response
[docs] @staticmethod def _gene_symbol(arguments: Dict[str, Any]) -> tuple[str | None, str | None]: value = arguments.get("gene_symbol") if not isinstance(value, str) or not value.strip(): return None, "gene_symbol must be a non-empty string" return value.strip().upper(), None
[docs] @staticmethod def _unknown_arguments(arguments: Dict[str, Any], allowed: set[str]) -> list[str]: return sorted(set(arguments) - allowed)
[docs] @staticmethod def _validate_variant_records(data: Any, gene_symbol: str) -> str | None: if not isinstance(data, list): return "LOVD variant response was not a list" for record in data: if not isinstance(record, dict): return "LOVD variant response contained a malformed record" symbol = record.get("symbol") if not isinstance(symbol, str) or symbol.upper() != gene_symbol: return "LOVD returned a variant for a different gene" dna = record.get("Variant/DNA") if dna is not None and ( not isinstance(dna, list) or any(not isinstance(value, str) for value in dna) ): return "LOVD returned malformed Variant/DNA values" return None
[docs] def _get_gene(self, arguments: Dict[str, Any]) -> Dict[str, Any]: """ Get gene information from LOVD by gene symbol. Returns gene details including HGNC ID, Entrez ID, chromosome location, RefSeq transcripts, and curation information. """ unknown = self._unknown_arguments(arguments, {"gene_symbol"}) if unknown: return { "status": "error", "error": f"Unrecognized parameter(s): {', '.join(unknown)}", } gene_symbol, error = self._gene_symbol(arguments) if error: return {"status": "error", "error": error} assert gene_symbol is not None url = f"{LOVD_BASE_URL}/genes/{quote(gene_symbol, safe='')}" try: response = self._make_request(url) if response.status_code == 404: return { "status": "error", "error": f"Gene '{gene_symbol}' not found in LOVD", } response.raise_for_status() data = response.json() if not isinstance(data, dict) or ( not isinstance(data.get("symbol"), str) or data["symbol"].upper() != gene_symbol ): return { "status": "error", "error": "LOVD returned a different or malformed gene record", "requested_symbol": gene_symbol, "returned_symbol": data.get("symbol") if isinstance(data, dict) else None, } return { "status": "success", "data": data, "metadata": { "query_symbol": gene_symbol, "returned_symbol_verified": True, "source_record_complete": True, }, } except requests.RequestException as e: return {"status": "error", "error": f"LOVD API request failed: {str(e)}"}
[docs] def _get_variants(self, arguments: Dict[str, Any]) -> Dict[str, Any]: """ Get variants for a gene from LOVD. Returns all curated variants including DNA/RNA/protein notation, genomic positions, and reporting counts. Use search_variants for filtered queries. """ unknown = self._unknown_arguments(arguments, {"gene_symbol"}) if unknown: return { "status": "error", "error": f"Unrecognized parameter(s): {', '.join(unknown)}", } gene_symbol, error = self._gene_symbol(arguments) if error: return {"status": "error", "error": error} assert gene_symbol is not None url = f"{LOVD_BASE_URL}/variants/{quote(gene_symbol, safe='')}" try: response = self._make_request(url) if response.status_code == 404: return { "status": "error", "error": f"Gene '{gene_symbol}' not found in LOVD", } response.raise_for_status() data = response.json() validation_error = self._validate_variant_records(data, gene_symbol) if validation_error: return {"status": "error", "error": validation_error} return { "status": "success", "data": data, "metadata": { "query_symbol": gene_symbol, "returned": len(data), "result_set_complete": True, "completeness_basis": "LOVD unpaginated gene-variant endpoint", "returned_gene_verified": True, }, } except requests.RequestException as e: return {"status": "error", "error": f"LOVD API request failed: {str(e)}"}
[docs] def _search_variants(self, arguments: Dict[str, Any]) -> Dict[str, Any]: """ Search variants in LOVD by DBID or DNA notation. Supports searching by: - Variant DBID (e.g., TP53_010464) - DNA notation without RefSeq prefix (e.g., c.*2609C>A) """ unknown = self._unknown_arguments( arguments, {"gene_symbol", "variant_dbid", "dna_notation"} ) if unknown: return { "status": "error", "error": f"Unrecognized parameter(s): {', '.join(unknown)}", } gene_symbol, error = self._gene_symbol(arguments) if error: return {"status": "error", "error": error} assert gene_symbol is not None dbid = arguments.get("variant_dbid") dna_notation = arguments.get("dna_notation") for name, value in (("variant_dbid", dbid), ("dna_notation", dna_notation)): if value is not None and (not isinstance(value, str) or not value.strip()): return { "status": "error", "error": f"{name} must be a non-empty string", } if bool(dbid) == bool(dna_notation): return { "status": "error", "error": "Provide exactly one of variant_dbid or dna_notation", } dbid = dbid.strip() if dbid else None dna_notation = dna_notation.strip() if dna_notation else None url = f"{LOVD_BASE_URL}/variants/{quote(gene_symbol, safe='')}" # Build search parameters search_params = {} if dbid: search_params["search_Variant/DBID"] = dbid else: # LOVD expects notation without RefSeq prefix (e.g., c.742C>T) search_params["search_Variant/DNA"] = dna_notation try: response = self._make_request(url, params=search_params) if response.status_code == 404: return { "status": "success", "data": [], "metadata": { "query_symbol": gene_symbol, "filter_type": "variant_dbid" if dbid else "dna_notation", "filter_value": dbid or dna_notation, "matches": 0, "filter_verified": True, "result_set_complete": True, "note": "LOVD uses HTTP 404 with an empty array for a valid gene filter with no matches.", }, } response.raise_for_status() data = response.json() validation_error = self._validate_variant_records(data, gene_symbol) if validation_error: return {"status": "error", "error": validation_error} if dbid: mismatches = [ record.get("Variant/DBID") for record in data if record.get("Variant/DBID") != dbid ] else: mismatches = [ record.get("Variant/DNA") for record in data if not any( value == dna_notation or value.endswith(f":{dna_notation}") for value in record.get("Variant/DNA", []) ) ] if mismatches: return { "status": "error", "error": "LOVD returned variants that did not match the requested filter", "mismatches": mismatches, } return { "status": "success", "data": data, "metadata": { "query_symbol": gene_symbol, "filter_type": "variant_dbid" if dbid else "dna_notation", "filter_value": dbid or dna_notation, "matches": len(data), "filter_verified": True, "result_set_complete": True, "returned_gene_verified": True, }, } except requests.RequestException as e: return {"status": "error", "error": f"LOVD API request failed: {str(e)}"}