Source code for tooluniverse.genomics_gene_search_tool

import requests
from .base_tool import BaseTool
from .tool_registry import register_tool


[docs] @register_tool("GWASGeneSearch") class GWASGeneSearch(BaseTool): """ Local tool wrapper for GWAS Catalog REST API. Searches associations by gene name. """
[docs] def __init__(self, tool_config): super().__init__(tool_config) self.base_url = "https://www.ebi.ac.uk/gwas/rest/api" self.session = requests.Session() self.session.headers.update( {"Accept": "application/json", "Content-Type": "application/json"} )
[docs] @staticmethod def _pvalue_sort_key(assoc): """Numeric p-value for sorting; unparseable/missing sort last.""" try: return float(assoc.get("p_value")) except (TypeError, ValueError): return float("inf")
[docs] def run(self, arguments): gene_name = arguments.get("gene_name") if not gene_name: return {"status": "error", "error": "Missing required parameter: gene_name"} # Default of 100 (was 5): a well-studied gene has hundreds of GWAS # associations (TCF7L2 has 902), and the API returns them UNSORTED, so a # size-5 default silently surfaced 5 arbitrary rows -- for TCF7L2 all 5 # were anthropometric (hip/waist/BMI) while its 37 flagship type-2- # diabetes associations were hidden, misleading a clinician into thinking # it is not a T2D locus. Also matches the sibling trait-search default. size = int(arguments.get("size") or arguments.get("limit") or 100) try: associations, total = self._fetch(gene_name, size) resolved_name = gene_name # `mapped_gene` matches the catalog's symbol literally, so writing an # interleukin the way clinicians do ("IL-23R") returns an ordinary # empty result rather than the 112 associations filed under "IL23R". # Genuinely hyphenated symbols are unaffected because they match on # the first attempt (HLA-A returns 107), so only retry after a miss -- # and say which spelling produced the answer. fallback_note = None if not associations and "-" in gene_name: candidate = gene_name.replace("-", "") retry_associations, retry_total = self._fetch(candidate, size) if retry_associations: associations, total = retry_associations, retry_total resolved_name = candidate fallback_note = ( f"No GWAS Catalog associations are filed under " f"'{gene_name}'; these results are for '{candidate}', " f"the unhyphenated symbol the catalog uses." ) result = { "gene_name": resolved_name, "association_count": len(associations), "associations": associations, "total_found": total, } if fallback_note: result["queried_gene_name"] = gene_name result["gene_name_note"] = fallback_note return result except requests.exceptions.RequestException as e: return {"status": "error", "error": f"Request failed: {str(e)}"} except Exception as e: return {"status": "error", "error": f"Unexpected error: {str(e)}"}
[docs] def _fetch(self, gene_name, size): """Return (associations sorted by p-value, total matches) for a symbol.""" response = self.session.get( f"{self.base_url}/v2/associations", params={"mapped_gene": gene_name, "size": size, "page": 0}, timeout=30, ) response.raise_for_status() data = response.json() associations = data.get("_embedded", {}).get("associations", []) # The API does not order by significance, but the tool description # promises the "strongest" associations -- sort the returned set by # p-value (most significant first) so the top rows are the strongest. associations = sorted(associations, key=self._pvalue_sort_key) return associations, data.get("page", {}).get("totalElements", 0)