bio-nexus-api / app /routers /pathways.py
Samad14's picture
Add sequencing pipeline (FASTQ -> QC -> alignment -> variant calling -> report)
da793ed verified
Raw
History Blame Contribute Delete
7.46 kB
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
@router.post("/search")
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)}
@router.post("/detail")
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', '')}",
}
@router.post("/kegg/search")
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)}
@router.post("/enrichment")
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