"""Assemble a case's ranked candidates: load the latest results, rank them, attach decisions. Everything for one job is loaded at once, which is fine for a gene panel or a chromosome — the size of case this demo handles. A whole genome would need the narrowing pushed into SQL. """ import uuid from collections.abc import Sequence from dataclasses import dataclass, field from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from app.models import Case, GenePhenotype, HpoTerm, Job, JobStatus, Variant from app.services import triage EMPTY_FUNNEL = triage.Funnel(total=0, rare=0, candidates=0, phenotype_matched=0) @dataclass(frozen=True) class CaseView: case: Case job: Job | None funnel: triage.Funnel candidates: list[triage.Candidate] evidence: triage.Evidence = field(default_factory=triage.Evidence) @property def labels(self) -> dict[str, str]: return {p.hpo_id: p.label for p in self.case.phenotypes} async def get_case(session: AsyncSession, case_id: uuid.UUID) -> Case | None: return await session.scalar( select(Case).where(Case.id == case_id).options(selectinload(Case.phenotypes)) ) async def latest_job( session: AsyncSession, case_id: uuid.UUID, *, status: JobStatus | None = None ) -> Job | None: stmt = select(Job).where(Job.case_id == case_id).order_by(Job.created_at.desc()).limit(1) if status is not None: stmt = stmt.where(Job.status == status) return await session.scalar(stmt) async def ontology_for( session: AsyncSession, genes: set[str], case_terms: Sequence[str] ) -> triage.Ontology: """The HPO reference data this case needs: each gene's terms, and each case term's specificity. Only the case's own terms need an information content: they are the denominator of the phenotype score, and a gene's other annotations never enter it. """ if not genes: return triage.Ontology() rows = await session.execute( select(GenePhenotype.gene_symbol, GenePhenotype.hpo_id).where( GenePhenotype.gene_symbol.in_(genes) ) ) gene_terms: dict[str, set[str]] = {} for gene, hpo_id in rows: gene_terms.setdefault(gene, set()).add(hpo_id) ic: dict[str, float] = {} if case_terms: weights = await session.execute( select(HpoTerm.hpo_id, HpoTerm.ic).where(HpoTerm.hpo_id.in_(case_terms)) ) ic = {hpo_id: float(value) for hpo_id, value in weights} return triage.Ontology(gene_terms=gene_terms, ic=ic) async def build(session: AsyncSession, case: Case) -> CaseView: job = await latest_job(session, case.id, status=JobStatus.succeeded) if job is None: return CaseView(case=case, job=None, funnel=EMPTY_FUNNEL, candidates=[]) variants = ( await session.scalars( select(Variant) .where(Variant.job_id == job.id) .options(selectinload(Variant.prediction), selectinload(Variant.decision)) ) ).all() case_terms = [p.hpo_id for p in case.phenotypes] ontology = await ontology_for(session, {v.gene for v in variants if v.gene}, case_terms) evidence = triage.Evidence( frequencies=job.has_frequencies, effect_scores=job.has_effect_scores ) return CaseView( case=case, job=job, funnel=triage.funnel(variants, case_terms, ontology, evidence), candidates=triage.rank(variants, case_terms, ontology, evidence), evidence=evidence, )