""" Stage 3: PDB Structure & UniProt ID Fetch Retrieves PDB structure IDs and UniProt accession numbers for target proteins. Includes file-based caching to avoid redundant API calls. """ import requests import json import logging import os from datetime import datetime from typing import List, Dict, Optional logger = logging.getLogger(__name__) class PdbStructurePipeline: def __init__(self, api_url: str = "https://rest.uniprot.org/uniprotkb/search", cache_dir: str = None): self.api_url = api_url if cache_dir is None: cache_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "data") self.cache_dir = cache_dir os.makedirs(self.cache_dir, exist_ok=True) def _get_cache_path(self, symbol: str) -> str: return os.path.join(self.cache_dir, f"cache_pdb_{symbol.lower()}.json") def _load_from_cache(self, symbol: str) -> Optional[Dict]: cache_path = self._get_cache_path(symbol) if os.path.exists(cache_path): try: with open(cache_path, "r") as f: data = json.load(f) if data.get("symbol") == symbol: logger.info(f" 📦 Loaded PDB data from cache for: {symbol}") return data except (json.JSONDecodeError, IOError) as e: logger.warning(f" ⚠️ Failed to read cache for {symbol}: {e}") return None def _save_to_cache(self, symbol: str, data: Dict): cache_path = self._get_cache_path(symbol) data["fetched_at"] = datetime.utcnow().isoformat() try: with open(cache_path, "w") as f: json.dump(data, f, indent=2) except IOError as e: logger.warning(f" ⚠️ Failed to write cache for {symbol}: {e}") def fetch_pdb_ids(self, targets: List[Dict]) -> List[Dict]: logger.info(f"Fetching PDB structures for {len(targets)} targets...") enriched = [] for t in targets: symbol = t["symbol"] cached = self._load_from_cache(symbol) if cached is not None: t["pdb_ids"] = cached.get("pdb_ids", []) t["uniprot_id"] = cached.get("uniprot_id", "") enriched.append(t) continue try: params = { "query": f"{symbol} AND taxonomy_id:9606", "format": "json", "size": 1, } response = requests.get(self.api_url, params=params, timeout=30) response.raise_for_status() data = response.json() uniprot_id = "" pdb_ids = [] if data.get("results") and len(data["results"]) > 0: result = data["results"][0] uniprot_id = result.get("primaryAccession", "") xrefs = result.get("uniProtKBCrossReferences", []) for xref in xrefs: if xref.get("database") == "PDB": pdb_id = xref.get("id") if pdb_id: pdb_ids.append(pdb_id) pdb_ids = pdb_ids[:3] t["pdb_ids"] = pdb_ids t["uniprot_id"] = uniprot_id if pdb_ids: logger.info(f" ✅ Found {len(pdb_ids)} PDB IDs for: {symbol}") else: logger.warning(f" ⚠️ No PDB structures found for: {symbol}") self._save_to_cache(symbol, { "symbol": symbol, "uniprot_id": uniprot_id, "pdb_ids": pdb_ids, }) except requests.exceptions.RequestException as e: logger.warning(f" ⚠️ Failed to fetch PDB for {symbol}: {e}") t["pdb_ids"] = [] t["uniprot_id"] = "" except Exception as e: logger.warning(f" ⚠️ Failed to fetch PDB for {symbol}: {e}") t["pdb_ids"] = [] t["uniprot_id"] = "" enriched.append(t) logger.info(f"✅ PDB fetch complete for {len(enriched)}/{len(targets)} targets") return enriched