Spaces:
Running
Running
Download bioai-platform/backend/app/routers/structures.py from Samad14/bio-nexus-api: direct link, hf CLI and curl.
- Browser
- Download file 6.9 kB
-
https://huggingface.co/spaces/Samad14/bio-nexus-api/resolve/main/bioai-platform/backend/app/routers/structures.py
- Command line
-
hf download hf://spaces/Samad14/bio-nexus-api/bioai-platform/backend/app/routers/structures.py
-
curl -L -o structures.py https://huggingface.co/spaces/Samad14/bio-nexus-api/resolve/main/bioai-platform/backend/app/routers/structures.py
6.9 kB
| import httpx | |
| import re | |
| from collections import defaultdict | |
| from fastapi import APIRouter, HTTPException | |
| from pydantic import BaseModel, Field | |
| from app.config import settings | |
| router = APIRouter() | |
| PDB_BASE = "https://data.rcsb.org/rest/v1/core" | |
| RCSB_SEARCH = "https://search.rcsb.org/rcsbsearch/v2/query" | |
| UNIPROT_BASE = "https://rest.uniprot.org/uniprotkb" | |
| class StructureSearchRequest(BaseModel): | |
| query: str = Field(..., min_length=1, description="PDB ID, UniProt accession, or keyword") | |
| class StructureInventoryRequest(BaseModel): | |
| pdb_id: str = Field(..., pattern=r"^[A-Za-z0-9]{4}$", description="Four-character PDB identifier") | |
| def _is_pdb_id(q: str) -> bool: | |
| return bool(re.fullmatch(r'[A-Za-z0-9]{4}', q)) | |
| async def _fetch_pdb(client: httpx.AsyncClient, pdb_id: str) -> dict | None: | |
| resp = await client.get(f"{PDB_BASE}/entry/{pdb_id}") | |
| if resp.status_code != 200: | |
| return None | |
| data = resp.json() | |
| return { | |
| "source": "pdb", | |
| "pdb_id": pdb_id.upper(), | |
| "title": data.get("struct", {}).get("title", ""), | |
| "method": (data.get("exptl", [{}])[0] or {}).get("method", ""), | |
| "resolution": (data.get("rcsb_entry_info", {}) or {}).get("resolution_combined", [{}])[0], | |
| "deposited": (data.get("rcsb_accession_info", {}) or {}).get("deposit_date", ""), | |
| "pdb_url": f"https://files.rcsb.org/view/{pdb_id.upper()}.pdb", | |
| } | |
| async def _fetch_alphafold(client: httpx.AsyncClient, accession: str) -> dict | None: | |
| af_url = f"{settings.ALPHAFOLD_DB_URL}/{accession}" | |
| resp = await client.get(af_url) | |
| if resp.status_code != 200: | |
| return None | |
| data = resp.json() | |
| if isinstance(data, list) and len(data) > 0: | |
| entry = data[0] | |
| return { | |
| "source": "alphafold", | |
| "uniprot_accession": accession.upper(), | |
| "pdb_url": entry.get("pdbUrl"), | |
| "cif_url": entry.get("cifUrl"), | |
| "confidence": entry.get("confidenceScore"), | |
| "model_created_date": entry.get("modelCreatedDate"), | |
| } | |
| return None | |
| async def _resolve_pdb_via_rcsb_uniprot(client: httpx.AsyncClient, accession: str) -> list[str]: | |
| """Find PDB entries associated with a UniProt accession via RCSB.""" | |
| payload = { | |
| "query": { | |
| "type": "terminal", | |
| "service": "text", | |
| "parameters": { | |
| "attribute": "rcsb_polymer_entity_container_identifiers.reference_sequence_identifiers.database_accession", | |
| "operator": "exact_match", | |
| "value": accession, | |
| }, | |
| }, | |
| "return_type": "entry", | |
| "request_options": { | |
| "paginate": { | |
| "start": 0, | |
| "rows": 50, | |
| }, | |
| }, | |
| } | |
| resp = await client.post(RCSB_SEARCH, json=payload, timeout=15) | |
| if resp.status_code != 200: | |
| return [] | |
| data = resp.json() | |
| return [hit.get("identifier", "") for hit in data.get("result_set", []) if hit.get("identifier")] | |
| async def _resolve_pdb_via_uniprot(client: httpx.AsyncClient, accession: str) -> list[str]: | |
| """Find PDB cross-references from UniProt entry.""" | |
| resp = await client.get(f"{UNIPROT_BASE}/{accession}", params={"format": "json"}, timeout=15) | |
| if resp.status_code != 200: | |
| return [] | |
| data = resp.json() | |
| refs = data.get("uniProtKBCrossReferences") or [] | |
| return [r.get("id", "") for r in refs if r.get("database") == "PDB"] | |
| async def fetch_structure(req: StructureSearchRequest): | |
| q = req.query.strip().upper() | |
| async with httpx.AsyncClient(timeout=15) as client: | |
| # Strategy 1: If it looks like a PDB ID, try PDB directly | |
| if _is_pdb_id(q): | |
| result = await _fetch_pdb(client, q) | |
| if result: | |
| return result | |
| # Strategy 2: Treat as a UniProt accession — look up PDB cross-refs | |
| pdb_ids = [] | |
| pdb_ids = await _resolve_pdb_via_rcsb_uniprot(client, q) | |
| if not pdb_ids: | |
| pdb_ids = await _resolve_pdb_via_uniprot(client, q) | |
| if pdb_ids: | |
| result = await _fetch_pdb(client, pdb_ids[0]) | |
| if result: | |
| return result | |
| # Strategy 3: Try AlphaFold as fallback | |
| af_result = await _fetch_alphafold(client, q) | |
| if af_result: | |
| return af_result | |
| raise HTTPException(status_code=404, detail="Structure not found in PDB or AlphaFold") | |
| async def search_pdb(req: StructureSearchRequest): | |
| payload = { | |
| "query": { | |
| "type": "terminal", | |
| "service": "text", | |
| "parameters": {"value": req.query}, | |
| }, | |
| "return_type": "entry", | |
| "request_options": { | |
| "paginate": { | |
| "start": 0, | |
| "rows": 20, | |
| }, | |
| }, | |
| } | |
| async with httpx.AsyncClient(timeout=15) as client: | |
| resp = await client.post(RCSB_SEARCH, json=payload) | |
| if resp.status_code != 200: | |
| raise HTTPException(status_code=502, detail="RCSB search failed") | |
| data = resp.json() | |
| results = [] | |
| for hit in data.get("result_set", []): | |
| results.append({ | |
| "pdb_id": hit.get("identifier", ""), | |
| "score": hit.get("score", 0), | |
| }) | |
| return {"results": results, "count": len(results)} | |
| async def structure_inventory(req: StructureInventoryRequest): | |
| """Return lightweight chain and non-polymer inventory for workbench controls.""" | |
| pdb_id = req.pdb_id.upper() | |
| async with httpx.AsyncClient(timeout=20) as client: | |
| response = await client.get(f"https://files.rcsb.org/download/{pdb_id}.pdb") | |
| if response.status_code != 200: | |
| raise HTTPException(status_code=404, detail=f"PDB file not found: {pdb_id}") | |
| chains: dict[str, set[tuple[str, str]]] = defaultdict(set) | |
| ligands: dict[tuple[str, str], dict] = {} | |
| for line in response.text.splitlines(): | |
| if len(line) < 27: | |
| continue | |
| record = line[:6].strip() | |
| if record not in {"ATOM", "HETATM"}: | |
| continue | |
| residue = line[17:20].strip() or "UNK" | |
| chain = line[21].strip() or "_" | |
| residue_id = f"{line[22:26].strip()}{line[26].strip()}" | |
| if record == "ATOM": | |
| chains[chain].add((residue, residue_id)) | |
| elif residue not in {"HOH", "WAT", "DOD"}: | |
| key = (residue, chain) | |
| ligands.setdefault(key, {"id": residue, "chain": chain, "residue_count": 0}) | |
| ligands[key]["residue_count"] += 1 | |
| return { | |
| "pdb_id": pdb_id, | |
| "chains": [ | |
| {"id": chain, "residue_count": len(residues)} | |
| for chain, residues in sorted(chains.items()) | |
| ], | |
| "ligands": sorted(ligands.values(), key=lambda ligand: (ligand["id"], ligand["chain"])), | |
| } | |