Download api/main.py from WellmatixGenAI/tp53-mutant-discovery: direct link, hf CLI and curl.
- Browser
- Download file 15.8 kB
-
https://huggingface.co/spaces/WellmatixGenAI/tp53-mutant-discovery/resolve/main/api/main.py
- Command line
-
hf download hf://spaces/WellmatixGenAI/tp53-mutant-discovery/api/main.py
-
curl -L -o main.py https://huggingface.co/spaces/WellmatixGenAI/tp53-mutant-discovery/resolve/main/api/main.py
15.8 kB
| """Versioned REST API for the TP53 discovery workflow.""" | |
| from __future__ import annotations | |
| import json | |
| from pathlib import Path | |
| from typing import Any | |
| from uuid import uuid4 | |
| import pandas as pd | |
| from fastapi import BackgroundTasks, FastAPI, HTTPException, Query | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.staticfiles import StaticFiles | |
| from pydantic import BaseModel, Field | |
| from utils.calculations import ( | |
| DEFAULT_WEIGHTS, | |
| SCORING_VERSION, | |
| apply_weights, | |
| bmut_from_score, | |
| consensus_score, | |
| empirical_docking_score, | |
| ingest_variant_frame, | |
| md_metrics, | |
| pocket_hydrophobicity, | |
| pocket_records, | |
| selectivity_from_scores, | |
| structure_record, | |
| ) | |
| from api import store | |
| DATA_DIR = Path(__file__).resolve().parent.parent / "data" | |
| app = FastAPI( | |
| title="TP53 Mutant Discovery API", | |
| version="1.0.0", | |
| description="Traceable variant-to-compound analysis with asynchronous jobs and versioned artifacts.", | |
| ) | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_methods=["GET", "POST"], | |
| allow_headers=["*"], | |
| ) | |
| class ProjectCreate(BaseModel): | |
| name: str = Field(min_length=1) | |
| cancer_type: str = "Lung Cancer" | |
| metadata: dict[str, Any] = Field(default_factory=dict) | |
| class VariantIngest(BaseModel): | |
| project_id: str | |
| records: list[dict[str, Any]] | |
| class VariantAnalyze(BaseModel): | |
| project_id: str | |
| class StructurePrepare(BaseModel): | |
| project_id: str | |
| variant_ids: list[str] | |
| source_preference: str = "experimental_then_template" | |
| class PocketDetect(BaseModel): | |
| project_id: str | |
| structure_ids: list[str] | |
| class CompoundScreen(BaseModel): | |
| project_id: str | |
| variant_id: str | |
| library_id: str = "platform-library-v1" | |
| model_id: str = "complementarity-v1" | |
| limit: int = Field(default=500, ge=1, le=2000) | |
| class DockingRun(BaseModel): | |
| project_id: str | |
| variant_id: str | |
| compounds: list[dict[str, Any]] | |
| protocol_id: str = "matched-wt-mutant-v1" | |
| class MDRun(BaseModel): | |
| project_id: str | |
| complexes: list[dict[str, Any]] | |
| protocol_id: str = "explicit-solvent-3x100ns-v1" | |
| class RankingCompute(BaseModel): | |
| project_id: str | |
| variant_id: str | |
| candidates: list[dict[str, Any]] | |
| weights: dict[str, float] = Field(default_factory=lambda: DEFAULT_WEIGHTS.copy()) | |
| def startup() -> None: | |
| store.init_db() | |
| def require_project(project_id: str) -> dict: | |
| project = store.get_project(project_id) | |
| if not project: | |
| raise HTTPException(404, f"Project {project_id} not found") | |
| return project | |
| def submit(background: BackgroundTasks, project_id: str, operation: str, payload: dict) -> dict: | |
| require_project(project_id) | |
| job = store.create_job(project_id, operation, payload) | |
| background.add_task(process_job, job["job_id"]) | |
| return {"job_id": job["job_id"], "status": "queued", "status_url": f"/jobs/{job['job_id']}"} | |
| def health() -> dict: | |
| return {"status": "ok", "version": app.version, "scoring_version": SCORING_VERSION} | |
| def create_project(body: ProjectCreate) -> dict: | |
| return store.create_project(body.name, body.cancer_type, body.metadata) | |
| def get_project(project_id: str) -> dict: | |
| return require_project(project_id) | |
| def variants_ingest(body: VariantIngest, background: BackgroundTasks) -> dict: | |
| return submit(background, body.project_id, "variants:ingest", body.model_dump()) | |
| def variant_analyze(variant_id: str, body: VariantAnalyze, background: BackgroundTasks) -> dict: | |
| return submit( | |
| background, | |
| body.project_id, | |
| "variant:analyze", | |
| {**body.model_dump(), "variant_id": variant_id}, | |
| ) | |
| def structures_prepare(body: StructurePrepare, background: BackgroundTasks) -> dict: | |
| return submit(background, body.project_id, "structures:prepare", body.model_dump()) | |
| def pockets_detect(body: PocketDetect, background: BackgroundTasks) -> dict: | |
| return submit(background, body.project_id, "pockets:detect", body.model_dump()) | |
| def compounds_screen(body: CompoundScreen, background: BackgroundTasks) -> dict: | |
| return submit(background, body.project_id, "compounds:screen", body.model_dump()) | |
| def docking_run(body: DockingRun, background: BackgroundTasks) -> dict: | |
| return submit(background, body.project_id, "docking:run", body.model_dump()) | |
| def md_run(body: MDRun, background: BackgroundTasks) -> dict: | |
| return submit(background, body.project_id, "md:run", body.model_dump()) | |
| def rankings_compute(body: RankingCompute, background: BackgroundTasks) -> dict: | |
| return submit(background, body.project_id, "rankings:compute", body.model_dump()) | |
| def get_job(job_id: str) -> dict: | |
| job = store.get_job(job_id) | |
| if not job: | |
| raise HTTPException(404, f"Job {job_id} not found") | |
| return job | |
| def project_jobs(project_id: str) -> list[dict]: | |
| require_project(project_id) | |
| return store.list_jobs(project_id) | |
| def project_artifacts(project_id: str) -> list[dict]: | |
| require_project(project_id) | |
| return store.list_artifacts(project_id) | |
| def artifact(artifact_id: str) -> dict: | |
| row = store.get_artifact(artifact_id) | |
| if not row: | |
| raise HTTPException(404, f"Artifact {artifact_id} not found") | |
| return row | |
| def project_report(project_id: str) -> dict: | |
| project = require_project(project_id) | |
| variants = store.list_records(project_id, "variant") | |
| structures = store.list_records(project_id, "structure") | |
| pockets = store.list_records(project_id, "pocket") | |
| rankings = store.list_records(project_id, "ranking") | |
| return { | |
| "project": project, | |
| "summary": { | |
| "variants": len(variants), | |
| "structures": len(structures), | |
| "pockets": len(pockets), | |
| "ranked_candidates": len(rankings), | |
| }, | |
| "top_candidates": sorted(rankings, key=lambda x: x.get("rescue_score", 0), reverse=True)[:10], | |
| "jobs": store.list_jobs(project_id), | |
| "artifacts": store.list_artifacts(project_id), | |
| "versions": {"api": app.version, "scoring": SCORING_VERSION}, | |
| } | |
| def process_job(job_id: str) -> None: | |
| job = store.get_job(job_id) | |
| if not job: | |
| return | |
| try: | |
| store.update_job(job_id, "running", log=f"Started {job['operation']}") | |
| handler = HANDLERS[job["operation"]] | |
| result = handler(job["project_id"], job["input"], job_id) | |
| store.update_job(job_id, "succeeded", output=result, log="Completed successfully") | |
| except Exception as exc: | |
| store.update_job(job_id, "failed", error=str(exc), log=f"Failed: {exc}") | |
| def handle_ingest(project_id: str, payload: dict, job_id: str) -> dict: | |
| frame = pd.DataFrame(payload["records"]) | |
| normalized = ingest_variant_frame(frame, require_project(project_id)["cancer_type"]) | |
| records = [] | |
| for _, row in normalized.iterrows(): | |
| item = _jsonable(row.to_dict()) | |
| item["variant_id"] = f"VAR-{uuid4().hex[:12]}" | |
| records.append(item) | |
| ids = store.add_records(project_id, "variant", records) | |
| artifact = store.add_artifact( | |
| project_id, "qc-report", "variant-qc.json", records, "application/json", job_id | |
| ) | |
| return {"variant_ids": ids, "normalized_records": records, "qc_artifact_id": artifact["artifact_id"]} | |
| def handle_analyze(project_id: str, payload: dict, job_id: str) -> dict: | |
| record = store.get_record(project_id, "variant", payload["variant_id"]) | |
| if not record: | |
| raise ValueError(f"Variant {payload['variant_id']} not found") | |
| return { | |
| "variant_id": payload["variant_id"], | |
| "hgvs_p": record["hgvs_p"], | |
| "priority": record["priority_score"], | |
| "class": record["functional_class"], | |
| "domain": record["domain"], | |
| "route": record["route"], | |
| "confidence": record["confidence"], | |
| "qc_flags": record["qc_flags"], | |
| } | |
| def handle_structures(project_id: str, payload: dict, job_id: str) -> dict: | |
| rows = [] | |
| for variant_id in payload["variant_ids"]: | |
| variant = store.get_record(project_id, "variant", variant_id) | |
| if not variant: | |
| raise ValueError(f"Variant {variant_id} not found") | |
| rec = _jsonable(structure_record(pd.Series(variant))) | |
| rec["structure_id"] = f"STR-{uuid4().hex[:12]}" | |
| rec["variant_id"] = variant_id | |
| rec["source_preference"] = payload["source_preference"] | |
| rows.append(rec) | |
| ids = store.add_records(project_id, "structure", rows) | |
| artifact = store.add_artifact(project_id, "structures", "structures.json", rows, "application/json", job_id) | |
| return {"structure_ids": ids, "structures": rows, "artifact_id": artifact["artifact_id"]} | |
| def handle_pockets(project_id: str, payload: dict, job_id: str) -> dict: | |
| rows = [] | |
| for structure_id in payload["structure_ids"]: | |
| structure = store.get_record(project_id, "structure", structure_id) | |
| if not structure: | |
| raise ValueError(f"Structure {structure_id} not found") | |
| for rec in pocket_records(structure): | |
| rec["pocket_id"] = f"POC-{uuid4().hex[:12]}" | |
| rec["structure_id"] = structure_id | |
| rows.append(_jsonable(rec)) | |
| ids = store.add_records(project_id, "pocket", rows) | |
| artifact = store.add_artifact(project_id, "pockets", "pockets.json", rows, "application/json", job_id) | |
| return {"pocket_ids": ids, "pockets": rows, "artifact_id": artifact["artifact_id"]} | |
| def handle_screen(project_id: str, payload: dict, job_id: str) -> dict: | |
| variant = store.get_record(project_id, "variant", payload["variant_id"]) | |
| if not variant: | |
| raise ValueError(f"Variant {payload['variant_id']} not found") | |
| source = pd.read_csv(DATA_DIR / "screening.csv") | |
| rows = source[source["variant"] == variant["hgvs_p"]].nlargest(payload["limit"], "ai_score") | |
| output = [_jsonable(row) for row in rows.to_dict("records")] | |
| artifact = store.add_artifact(project_id, "screening", "screening.json", output, "application/json", job_id) | |
| return { | |
| "variant_id": payload["variant_id"], | |
| "model_id": payload["model_id"], | |
| "library_id": payload["library_id"], | |
| "candidates": output, | |
| "artifact_id": artifact["artifact_id"], | |
| } | |
| def handle_docking(project_id: str, payload: dict, job_id: str) -> dict: | |
| variant = store.get_record(project_id, "variant", payload["variant_id"]) | |
| if not variant: | |
| raise ValueError(f"Variant {payload['variant_id']} not found") | |
| structure = next( | |
| (s for s in store.list_records(project_id, "structure") if s["variant_id"] == payload["variant_id"]), | |
| None, | |
| ) | |
| if not structure: | |
| structure = _jsonable(structure_record(pd.Series(variant))) | |
| hyd = pocket_hydrophobicity(int(variant["position"]), variant["hgvs_p"]) | |
| rows = [] | |
| for compound in payload["compounds"]: | |
| required = ("compound_id", "mw", "logp", "tpsa", "rotbonds") | |
| missing = [key for key in required if key not in compound] | |
| if missing: | |
| raise ValueError(f"Compound missing {missing}") | |
| dmut = empirical_docking_score( | |
| compound["mw"], compound["logp"], compound["tpsa"], compound["rotbonds"], | |
| structure.get("pocket_vol_mut") or 165, hyd, | |
| ) | |
| dwt = empirical_docking_score( | |
| compound["mw"], compound["logp"], compound["tpsa"], compound["rotbonds"], | |
| structure.get("pocket_vol_wt") or 165, 0.30, wt_pocket=True, | |
| ) | |
| rows.append({ | |
| **compound, | |
| "docking_id": f"DOCK-{uuid4().hex[:12]}", | |
| "variant_id": payload["variant_id"], | |
| "dock_mut": round(dmut, 3), | |
| "dock_wt": round(dwt, 3), | |
| "delta_dock": round(dmut - dwt, 3), | |
| "mutant_preference": round(selectivity_from_scores(dmut, dwt), 3), | |
| "protocol_version": payload["protocol_id"], | |
| "units": "kcal/mol", | |
| }) | |
| ids = store.add_records(project_id, "docking", rows) | |
| artifact = store.add_artifact(project_id, "docking", "docking.json", rows, "application/json", job_id) | |
| return {"docking_ids": ids, "poses": rows, "artifact_id": artifact["artifact_id"]} | |
| def handle_md(project_id: str, payload: dict, job_id: str) -> dict: | |
| rows = [] | |
| for complex_data in payload["complexes"]: | |
| for key in ("complex_id", "dock_mut"): | |
| if key not in complex_data: | |
| raise ValueError(f"Complex missing {key}") | |
| bmut = bmut_from_score(complex_data["dock_mut"]) | |
| selectivity = float(complex_data.get("mutant_preference", 0)) | |
| metrics = md_metrics( | |
| complex_data["dock_mut"], complex_data.get("rotbonds", 4), bmut, selectivity | |
| ) | |
| rows.append({ | |
| "md_id": f"MD-{uuid4().hex[:12]}", | |
| **complex_data, | |
| **metrics, | |
| "replicas": 3, | |
| "duration_ns": 100, | |
| "protocol_version": payload["protocol_id"], | |
| }) | |
| ids = store.add_records(project_id, "md", rows) | |
| artifact = store.add_artifact(project_id, "md", "md-summary.json", rows, "application/json", job_id) | |
| return {"md_ids": ids, "results": rows, "artifact_id": artifact["artifact_id"]} | |
| def handle_rankings(project_id: str, payload: dict, job_id: str) -> dict: | |
| candidates = pd.DataFrame(payload["candidates"]) | |
| required = list(DEFAULT_WEIGHTS) | |
| missing = [col for col in required if col not in candidates.columns] | |
| if missing: | |
| raise ValueError(f"Candidates missing score components: {missing}") | |
| if "variant" not in candidates: | |
| candidates["variant"] = payload["variant_id"] | |
| if "confidence" not in candidates: | |
| candidates["confidence"] = 0.5 | |
| if "admet_flag" not in candidates: | |
| candidates["admet_flag"] = "Watch" | |
| ranked = apply_weights(candidates, payload["weights"]) | |
| rows = [] | |
| for rec in ranked.to_dict("records"): | |
| rec = _jsonable(rec) | |
| rec["ranking_id"] = f"RNK-{uuid4().hex[:12]}" | |
| rec["variant_id"] = payload["variant_id"] | |
| rec["scoring_version"] = SCORING_VERSION | |
| rows.append(rec) | |
| ids = store.add_records(project_id, "ranking", rows) | |
| artifact = store.add_artifact(project_id, "ranking", "ranking.json", rows, "application/json", job_id) | |
| return {"ranking_ids": ids, "shortlist": rows[:10], "artifact_id": artifact["artifact_id"]} | |
| HANDLERS = { | |
| "variants:ingest": handle_ingest, | |
| "variant:analyze": handle_analyze, | |
| "structures:prepare": handle_structures, | |
| "pockets:detect": handle_pockets, | |
| "compounds:screen": handle_screen, | |
| "docking:run": handle_docking, | |
| "md:run": handle_md, | |
| "rankings:compute": handle_rankings, | |
| } | |
| def _jsonable(value: Any) -> Any: | |
| if isinstance(value, dict): | |
| return {k: _jsonable(v) for k, v in value.items()} | |
| if isinstance(value, list): | |
| return [_jsonable(v) for v in value] | |
| if pd.isna(value): | |
| return None | |
| if hasattr(value, "item"): | |
| return value.item() | |
| return value | |
| # Mounted last so API routes take precedence; Docker serves API and dashboard together. | |
| app.mount("/", StaticFiles(directory=Path(__file__).resolve().parent.parent, html=True), name="dashboard") | |