Initial release: rarelens platform skeleton (AGPL-3.0)
ci / api (push) Failing after 10s
ci / terraform (push) Failing after 11s
ci / web (push) Failing after 35s
ci / pipeline (push) Failing after 2m29s
ci / images (api) (push) Skipped
ci / images (ml) (push) Skipped
ci / images (pipeline) (push) Skipped
ci / images (web) (push) Skipped

End-to-end variant interpretation platform for rare genetic disease research:
SvelteKit UI, FastAPI + PostgreSQL API, Nextflow/Ensembl VEP pipeline,
LightGBM pathogenicity scoring with MLflow, K8s/ArgoCD/GCP infrastructure.
Public test data only; no clinical claims.
This commit is contained in:
2026-09-11 16:55:35 +01:00
commit 5463f489a3
74 changed files with 4597 additions and 0 deletions
View File
+18
View File
@@ -0,0 +1,18 @@
import uuid
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from app.db import get_session
from app.models import Job
from app.schemas import JobOut
router = APIRouter()
@router.get("/{job_id}", response_model=JobOut)
async def get_job(job_id: uuid.UUID, session: AsyncSession = Depends(get_session)):
job = await session.get(Job, job_id)
if job is None:
raise HTTPException(404, "job not found")
return job
+16
View File
@@ -0,0 +1,16 @@
import uuid
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.db import get_session
from app.services.scoring import score_job
router = APIRouter()
@router.post("/score/{job_id}")
async def score(job_id: uuid.UUID, session: AsyncSession = Depends(get_session)) -> dict:
"""Load the registered MLflow model and score every variant of a job."""
n = await score_job(job_id, session)
return {"job_id": str(job_id), "scored": n}
+49
View File
@@ -0,0 +1,49 @@
import uuid
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.db import get_session
from app.models import Job, Sample
from app.schemas import JobOut, SampleCreate, SampleOut
from app.services.events import publish_vcf_uploaded
router = APIRouter()
@router.get("", response_model=list[SampleOut])
async def list_samples(session: AsyncSession = Depends(get_session)):
result = await session.scalars(select(Sample).order_by(Sample.created_at.desc()))
return result.all()
@router.post("", response_model=SampleOut, status_code=status.HTTP_201_CREATED)
async def create_sample(payload: SampleCreate, session: AsyncSession = Depends(get_session)):
sample = Sample(**payload.model_dump())
session.add(sample)
await session.commit()
await session.refresh(sample)
return sample
@router.get("/{sample_id}/jobs", response_model=list[JobOut])
async def list_jobs(sample_id: uuid.UUID, session: AsyncSession = Depends(get_session)):
result = await session.scalars(
select(Job).where(Job.sample_id == sample_id).order_by(Job.created_at.desc())
)
return result.all()
@router.post("/{sample_id}/annotate", response_model=JobOut, status_code=status.HTTP_202_ACCEPTED)
async def annotate(sample_id: uuid.UUID, session: AsyncSession = Depends(get_session)):
sample = await session.get(Sample, sample_id)
if sample is None:
raise HTTPException(404, "sample not found")
job = Job(sample_id=sample.id)
session.add(job)
await session.commit()
await session.refresh(job)
# Emits to Pub/Sub in GCP; runs the Nextflow pipeline inline for local dev.
await publish_vcf_uploaded(job_id=job.id, vcf_uri=sample.vcf_uri)
return job
+43
View File
@@ -0,0 +1,43 @@
import uuid
from fastapi import APIRouter, Depends, Query
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.db import get_session
from app.models import Prediction, Variant
from app.schemas import VariantPage
router = APIRouter()
@router.get("", response_model=VariantPage)
async def list_variants(
job_id: uuid.UUID,
gene: str | None = None,
impact: str | None = Query(None, pattern="^(HIGH|MODERATE|LOW|MODIFIER)$"),
max_af: float | None = Query(None, ge=0, le=1),
min_score: float | None = Query(None, ge=0, le=1),
limit: int = Query(50, le=500),
offset: int = 0,
session: AsyncSession = Depends(get_session),
):
stmt = select(Variant).where(Variant.job_id == job_id)
if gene:
stmt = stmt.where(Variant.gene == gene.upper())
if impact:
stmt = stmt.where(Variant.impact == impact)
if max_af is not None:
stmt = stmt.where((Variant.gnomad_af.is_(None)) | (Variant.gnomad_af <= max_af))
if min_score is not None:
stmt = stmt.join(Prediction, Prediction.variant_id == Variant.id).where(Prediction.score >= min_score)
total = await session.scalar(select(func.count()).select_from(stmt.subquery()))
rows = await session.scalars(
stmt.options(selectinload(Variant.prediction))
.order_by(Variant.chrom, Variant.pos)
.limit(limit)
.offset(offset)
)
return VariantPage(items=rows.all(), total=total or 0, limit=limit, offset=offset)