import re import uuid from datetime import datetime from pathlib import PurePosixPath from typing import Literal from pydantic import BaseModel, ConfigDict, Field, field_validator from app.config import settings from app.models import JobStatus class ORMModel(BaseModel): model_config = ConfigDict(from_attributes=True) Assembly = Literal["GRCh38", "GRCh37"] VCF_SUFFIXES = (".vcf", ".vcf.gz", ".vcf.bgz", ".bcf") GCS_URI = re.compile(r"gs://[a-z0-9][a-z0-9._-]{1,220}[a-z0-9]/\S+") class SampleCreate(BaseModel): name: str = Field(min_length=1, max_length=120) vcf_uri: str # Passed to VEP --assembly; the VEP cache must contain it. assembly: Assembly = "GRCh38" @field_validator("vcf_uri") @classmethod def _gcs_object_or_file_under_data_root(cls, v: str) -> str: # The URI becomes a Nextflow argument and a path the pipeline reads: accept a GCS object or # a file under the local data root, never something that parses as an option. if not v or any(ord(c) < 32 for c in v): raise ValueError("vcf_uri must be a non-empty single line") if not v.lower().endswith(VCF_SUFFIXES): raise ValueError(f"vcf_uri must end in one of {', '.join(VCF_SUFFIXES)}") if v.startswith("gs://"): if not GCS_URI.fullmatch(v): raise ValueError("vcf_uri is not a valid gs://bucket/object URI") return v path, root = PurePosixPath(v), PurePosixPath(settings.local_data_root) if not path.is_absolute() or ".." in path.parts or not path.is_relative_to(root): raise ValueError(f"local VCFs must be absolute paths under {root}") return v class SampleOut(ORMModel): id: uuid.UUID name: str vcf_uri: str assembly: str created_at: datetime class JobOut(ORMModel): id: uuid.UUID sample_id: uuid.UUID status: JobStatus workflow_ref: str | None vep_version: str | None log: str | None created_at: datetime finished_at: datetime | None class PredictionOut(ORMModel): model_name: str model_version: str score: float class VariantOut(ORMModel): id: int chrom: str pos: int ref: str alt: str gene: str | None consequence: str | None impact: str | None hgvsc: str | None hgvsp: str | None gnomad_af: float | None clinvar_sig: str | None prediction: PredictionOut | None = None class ScoreOut(BaseModel): job_id: uuid.UUID scored: int model_version: str class VariantPage(BaseModel): items: list[VariantOut] total: int limit: int offset: int