Spaces:
Running
Running
| import httpx | |
| from fastapi import APIRouter, HTTPException | |
| from pydantic import BaseModel, Field | |
| router = APIRouter() | |
| REACTOME_BASE = "https://reactome.org/ContentService" | |
| class PathwaySearchRequest(BaseModel): | |
| query: str = Field(..., min_length=2, description="Gene name or protein identifier") | |
| species: str = Field("Homo sapiens", description="Species name") | |
| class PathwayDetailRequest(BaseModel): | |
| pathway_id: str = Field(..., min_length=1, description="Reactome pathway ID (e.g. R-HSA-1640170)") | |
| class KEGGSearchRequest(BaseModel): | |
| query: str = Field(..., min_length=2, description="Gene name or keyword") | |
| class EnrichmentRequest(BaseModel): | |
| identifiers: list[str] = Field(..., min_length=1, description="List of gene or protein identifiers") | |
| def _extract_entries(data: dict) -> list[dict]: | |
| entries = [] | |
| for group in data.get("results", []): | |
| entries.extend(group.get("entries", [])) | |
| return entries | |
| async def search_pathways(req: PathwaySearchRequest): | |
| async with httpx.AsyncClient(timeout=15) as client: | |
| resp = await client.get( | |
| f"{REACTOME_BASE}/search/query", | |
| params={"query": req.query, "species": req.species, "types": "Pathway"}, | |
| ) | |
| if resp.status_code != 200: | |
| raise HTTPException(status_code=502, detail="Reactome search failed") | |
| data = resp.json() | |
| results = [] | |
| seen = set() | |
| for item in _extract_entries(data): | |
| st_id = item.get("stId", "") | |
| if not st_id or st_id in seen: | |
| continue | |
| seen.add(st_id) | |
| results.append({ | |
| "pathway_id": st_id, | |
| "name": item.get("displayName", item.get("name", "")), | |
| "species": item.get("species", ["Unknown"])[0] if isinstance(item.get("species"), list) else (item.get("species", {}) or {}).get("name", ""), | |
| "url": f"https://reactome.org/content/detail/{st_id}", | |
| }) | |
| if not results: | |
| async with httpx.AsyncClient(timeout=15) as client: | |
| resp = await client.get( | |
| f"{REACTOME_BASE}/search/fireworks", | |
| params={"query": req.query, "species": req.species}, | |
| ) | |
| if resp.status_code == 200: | |
| data = resp.json() | |
| for item in data.get("entries", []): | |
| st_id = item.get("stId", "") | |
| if not st_id or st_id in seen: | |
| continue | |
| seen.add(st_id) | |
| results.append({ | |
| "pathway_id": st_id, | |
| "name": item.get("name", ""), | |
| "species": item.get("species", ["Unknown"])[0] if isinstance(item.get("species"), list) else "", | |
| "url": f"https://reactome.org/content/detail/{st_id}", | |
| }) | |
| return {"results": results, "count": len(results)} | |
| async def pathway_detail(req: PathwayDetailRequest): | |
| async with httpx.AsyncClient(timeout=15) as client: | |
| resp = await client.get(f"{REACTOME_BASE}/data/fireworks/{req.pathway_id}") | |
| if resp.status_code != 200: | |
| raise HTTPException(status_code=404, detail="Pathway not found") | |
| data = resp.json() | |
| return { | |
| "pathway_id": data.get("stId", ""), | |
| "name": data.get("name", ""), | |
| "species": (data.get("species", {}) or {}).get("name", ""), | |
| "description": data.get("definition", ""), | |
| "url": f"https://reactome.org/content/detail/{data.get('stId', '')}", | |
| } | |
| async def kegg_search(req: KEGGSearchRequest): | |
| query = req.query.strip() | |
| results = [] | |
| seen = set() | |
| q_upper = query.upper() | |
| async with httpx.AsyncClient(timeout=15) as client: | |
| find_resp = await client.get(f"https://rest.kegg.jp/find/hsa/{query}") | |
| kegg_gene_id = None | |
| if find_resp.status_code == 200: | |
| for line in find_resp.text.strip().split("\n"): | |
| parts = line.split("\t", 1) | |
| if len(parts) != 2: | |
| continue | |
| gene_id = parts[0] | |
| after_tab = parts[1] | |
| symbols_part = after_tab.split(";")[0] | |
| symbols = [s.strip().upper() for s in symbols_part.split(",")] | |
| if q_upper in symbols: | |
| kegg_gene_id = gene_id | |
| break | |
| if kegg_gene_id: | |
| gene_resp = await client.get(f"https://rest.kegg.jp/get/{kegg_gene_id}") | |
| if gene_resp.status_code == 200: | |
| in_pathway = False | |
| for line in gene_resp.text.split("\n"): | |
| if line.startswith("PATHWAY"): | |
| in_pathway = True | |
| elif in_pathway: | |
| s = line.strip() | |
| if s == "": | |
| continue | |
| if not line.startswith(" "): | |
| in_pathway = False | |
| continue | |
| if not in_pathway: | |
| continue | |
| rest = line[9:] if line.startswith("PATHWAY") else line.strip() | |
| rest = rest.strip() | |
| parts = rest.split(None, 1) | |
| if len(parts) == 2: | |
| pid, pname = parts | |
| if pid not in seen: | |
| seen.add(pid) | |
| results.append({ | |
| "pathway_id": pid, | |
| "name": pname, | |
| "organism": "Homo sapiens", | |
| "url": f"https://www.kegg.jp/entry/{pid}", | |
| "image_url": f"https://rest.kegg.jp/get/{pid}/image", | |
| }) | |
| if not results: | |
| text_resp = await client.get(f"https://rest.kegg.jp/find/pathway/{query}") | |
| if text_resp.status_code == 200: | |
| for line in text_resp.text.strip().split("\n"): | |
| parts = line.split("\t", 1) | |
| if len(parts) == 2: | |
| pid = parts[0] | |
| name = parts[1].split(" - ")[0] | |
| organism = parts[1].split(" - ")[-1] if " - " in parts[1] else "" | |
| if pid not in seen: | |
| seen.add(pid) | |
| results.append({ | |
| "pathway_id": pid, | |
| "name": name, | |
| "organism": organism if organism != name else "Homo sapiens", | |
| "url": f"https://www.kegg.jp/entry/{pid}", | |
| "image_url": f"https://rest.kegg.jp/get/{pid}/image", | |
| }) | |
| return {"results": results, "count": len(results)} | |
| async def pathway_enrichment(req: EnrichmentRequest): | |
| from app.services.pathway_enrichment import run_enrichment as _run_enrichment | |
| result = await _run_enrichment(req.identifiers) | |
| if result is None: | |
| raise HTTPException(status_code=502, detail="Enrichment analysis failed") | |
| return result | |