"""src/bio_knowledge/protein_sequence_retriever.py Protein Sequence Retrieval (UniProt) This module retrieves REAL protein sequences for UniProt accessions. Primary mode: - Fetch from UniProt REST API (https://rest.uniprot.org/) and cache locally. Offline mode: - If the sequence is already cached, it will be used without network access. This is designed to eliminate placeholder/synthetic sequences for quantum protein analysis while keeping the system usable in offline environments once data has been cached. """ from __future__ import annotations import json import time import urllib.error import urllib.request from dataclasses import dataclass from pathlib import Path from typing import Dict, Optional @dataclass class ProteinSequenceRecord: uniprot_id: str sequence: str fetched_at: float source_url: str def to_dict(self) -> Dict: return { "uniprot_id": self.uniprot_id, "sequence": self.sequence, "fetched_at": self.fetched_at, "source_url": self.source_url, } class ProteinSequenceRetriever: def __init__(self, cache_dir: str = "./protein_sequence_cache"): self.cache_dir = Path(cache_dir) self.cache_dir.mkdir(parents=True, exist_ok=True) def _cache_path(self, uniprot_id: str) -> Path: return self.cache_dir / f"{uniprot_id}.json" def get_sequence(self, uniprot_id: str, allow_network: bool = True) -> Optional[ProteinSequenceRecord]: uniprot_id = uniprot_id.strip() cached = self._read_cache(uniprot_id) if cached is not None: return cached if not allow_network: return None return self._fetch_and_cache(uniprot_id) def _read_cache(self, uniprot_id: str) -> Optional[ProteinSequenceRecord]: path = self._cache_path(uniprot_id) if not path.exists(): return None data = json.loads(path.read_text()) seq = data.get("sequence", "") if not seq: return None return ProteinSequenceRecord( uniprot_id=data.get("uniprot_id", uniprot_id), sequence=seq, fetched_at=float(data.get("fetched_at", 0.0)), source_url=data.get("source_url", ""), ) def _fetch_and_cache(self, uniprot_id: str) -> Optional[ProteinSequenceRecord]: url = f"https://rest.uniprot.org/uniprotkb/{uniprot_id}.fasta" req = urllib.request.Request(url, headers={"User-Agent": "cto.new-cancer-research/1.0"}) try: with urllib.request.urlopen(req, timeout=20) as resp: fasta = resp.read().decode("utf-8") except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError): return None sequence = self._parse_fasta_sequence(fasta) if not sequence: return None record = ProteinSequenceRecord( uniprot_id=uniprot_id, sequence=sequence, fetched_at=time.time(), source_url=url, ) self._cache_path(uniprot_id).write_text(json.dumps(record.to_dict(), indent=2)) return record @staticmethod def _parse_fasta_sequence(fasta: str) -> str: lines = [ln.strip() for ln in fasta.splitlines() if ln.strip()] seq_lines = [ln for ln in lines if not ln.startswith(">")] return "".join(seq_lines).strip()