- scripts/pgx/fetch_sources.sh: CPIC API tables (CC0) into a dated folder with version and hashes. - simurg/pgx/db.py: CPIC HGVS locations and values -> left-normalised VCF variants (SNV, del, ins, dup, repeats, delins, chrM); IUPAC values are core-allele wildcards; alleles without definitions are flagged. - pipeline/pgx_db.py: deterministic SQLite (content-hash name), probe_map via the SNV VCF site mapper; GSA v4 PGx: 22 genes, 1354 alleles, 5002 probe links; two builds byte-identical.
253 lines
16 KiB
Python
253 lines
16 KiB
Python
"""Stable message catalogue and exception types (spec CLI-014, CLI-016).
|
|
|
|
Every error or warning SimurgArray emits has a stable ``code``. The human-readable text may change between
|
|
versions; the code may not. ``docs/messages.md`` is generated from :data:`CATALOGUE`
|
|
(``python -m simurg.core.errors``).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from enum import IntEnum
|
|
from typing import Literal
|
|
|
|
Level = Literal["error", "warning", "info"]
|
|
|
|
|
|
class ExitCode(IntEnum):
|
|
"""Process exit codes (spec CLI-014)."""
|
|
|
|
OK = 0
|
|
INTERNAL = 1
|
|
USAGE = 2 # usage or pre-flight validation error; no per-sample work started
|
|
PARTIAL = 3 # >= 1 sample failed and >= 1 succeeded
|
|
ALL_FAILED = 4 # run completed but no sample succeeded
|
|
SIGINT = 130
|
|
SIGPIPE = 141 # the reader of stdout went away (e.g. `simurg inspect … | head`); nothing is reported
|
|
SIGTERM = 143
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class MessageDef:
|
|
code: str
|
|
level: Level
|
|
summary: str
|
|
spec: str
|
|
|
|
|
|
def _m(code: str, level: Level, summary: str, spec: str) -> tuple[str, MessageDef]:
|
|
return code, MessageDef(code, level, summary, spec)
|
|
|
|
|
|
CATALOGUE: dict[str, MessageDef] = dict(
|
|
[
|
|
# --- CLI / run control -------------------------------------------------------------------
|
|
_m("E_NO_INPUT", "error", "Neither an input folder nor a sample sheet was given", "CLI-006"),
|
|
_m("E_NO_INPUT_FOLDER", "error", "Sample sheet has no RootFolder and no folder was given", "CLI-006"),
|
|
_m("E_OUTPUT_EXISTS", "error", "Outputs already exist; use --force or --resume", "CLI-010"),
|
|
_m("E_CONFIG_KEY", "error", "Unknown key in --config YAML", "CLI-019"),
|
|
_m("E_CONFIG_PARSE", "error", "--config file is not valid YAML", "CLI-019"),
|
|
_m("E_FILE_NOT_FOUND", "error", "Input file or folder does not exist", "CLI-016"),
|
|
_m("E_FILE_UNREADABLE", "error", "Input file cannot be read", "CLI-016"),
|
|
_m("E_INTERNAL", "error", "Internal error (bug); rerun with --debug and report", "CLI-014"),
|
|
# --- sample sheet (IN-001 .. IN-017) -------------------------------------------------------
|
|
_m("E_SHEET_DELIMITER", "error", "Sample sheet is not comma-delimited CSV", "IN-001"),
|
|
_m("E_SHEET_EMPTY", "error", "Sample sheet has no data rows", "IN-001"),
|
|
_m("E_SHEET_NO_KEY_COLUMNS", "error", "Sample sheet lacks SentrixBarcode_A/SentrixPosition_A", "IN-002"),
|
|
_m("E_SHEET_DUP_COLUMN", "error", "Duplicate column (column names are case-insensitive)", "IN-003"),
|
|
_m("W_SHEET_EMPTY_COLUMN", "warning", "Columns with empty headers are ignored", "IN-004"),
|
|
_m("E_SHEET_RESERVED", "error", "Sample sheet column collides with an output column", "IN-005"),
|
|
_m("E_SHEET_TOO_LONG", "error", "Sample sheet name or value longer than 500 characters", "IN-006"),
|
|
_m("E_SHEET_BARCODE_FORMAT", "error", "Invalid SentrixBarcode_A / SentrixPosition_A value", "IN-007"),
|
|
_m("W_SHEET_BARCODE_REPAIRED", "warning", "Barcode written as a decimal was repaired", "IN-007"),
|
|
_m("W_SHEET_ROOTFOLDER_OVERRIDDEN", "warning", "CLI folder overrides the sheet RootFolder", "IN-008"),
|
|
_m("E_SHEET_DUP_SAMPLE", "error", "The same barcode/position appears in several rows", "IN-009"),
|
|
_m("E_SHEET_NAME_EMPTY", "error", "Sample_Name is empty in at least one row", "IN-010"),
|
|
_m("E_SHEET_NAME_COLLISION", "error", "Sample names are not unique or clash with sample IDs", "IN-010"),
|
|
_m("W_SHEET_SEX_CONFLICT", "warning", "Reported-sex columns disagree", "IN-014"),
|
|
# --- IDAT / GTC discovery & parsing (IN-020 .. IN-033) --------------------------------------
|
|
_m("E_DUP_IDAT", "error", "Several IDAT pairs resolve to the same sample", "IN-022"),
|
|
_m("W_DUP_IDAT_RESOLVED", "warning", "Duplicate IDAT pair resolved by --on-duplicate-idat", "IN-022"),
|
|
_m("W_IDAT_UNPAIRED", "warning", "IDAT channel file without its partner", "IN-021"),
|
|
_m("E_IDAT_FORMAT", "error", "IDAT file is malformed", "IN-023"),
|
|
_m("E_IDAT_ENCRYPTED", "error", "Encrypted (version 1) IDATs are not supported", "IN-023"),
|
|
_m("E_IDAT_PAIR_MISMATCH", "error", "Red and Grn IDATs of a sample do not match", "IN-023"),
|
|
_m("W_IDAT_NAME_MISMATCH", "warning", "IDAT barcode/position disagrees with its file name", "IN-023"),
|
|
_m("E_IDAT_MANIFEST_MISMATCH", "error", "IDATs and manifest belong to different products", "IN-027"),
|
|
_m("W_IDAT_LOW_ADDRESS_COVERAGE", "warning", "Some manifest addresses are absent from the IDAT", "IN-027"),
|
|
_m("W_IDAT_MANIFEST_NAME", "warning", "IDAT names a different manifest than the BPM", "IN-027"),
|
|
_m("W_IDAT_NO_SCAN_TIME", "warning", "IDAT has no scan entry in its run info", "IN-024"),
|
|
_m("I_UNLISTED_IDATS", "info", "IDAT pairs not listed in the sample sheet are ignored", "IN-016"),
|
|
_m("E_DUP_GTC", "error", "Several GTC files resolve to the same sample", "IN-030"),
|
|
_m("E_GTC_CORRUPT", "warning", "GTC file failed validation; sample skipped", "IN-032"),
|
|
_m("W_GTC_FOREIGN", "warning", "GTC was produced by another tool", "IN-031"),
|
|
_m("E_GTC_UNSUITABLE", "error", "GTC lacks fields required by this command", "IN-031"),
|
|
# --- manifests & cluster files (IN-040 .. IN-047) -------------------------------------------
|
|
_m("E_BPM_FORMAT", "error", "BPM manifest is malformed or unsupported", "IN-040"),
|
|
_m("E_CSV_MANIFEST_FORMAT", "error", "CSV manifest is malformed", "IN-041"),
|
|
_m("E_BPM_CSV_MISMATCH", "error", "BPM and CSV manifests describe different loci", "IN-042"),
|
|
_m("E_EGT_MANIFEST_MISMATCH", "error", "Cluster file lacks loci of the manifest", "IN-043"),
|
|
_m("E_EGT_LOAD", "error", "Cluster (EGT) file cannot be loaded", "IN-044"),
|
|
_m("W_EGT_NAME", "warning", "Cluster file name is not portable", "IN-045"),
|
|
_m("W_MANIFEST_NO_REFSTRAND", "warning", "Manifest lacks RefStrand annotation", "IN-040"),
|
|
# --- reference genome and VCF export (IN-060 .. IN-065, OUT-010 .. OUT-043) --------------------------
|
|
_m("E_FASTA_FORMAT", "error", "FASTA file is malformed", "IN-060"),
|
|
_m("E_FAI_MISMATCH", "error", "FASTA index does not match the FASTA", "IN-060"),
|
|
_m("E_BUILD_MISMATCH", "error", "Manifest genome build differs from the FASTA build", "IN-063"),
|
|
_m("W_SKIP_UNMAPPED", "warning", "Locus without a genomic position was skipped", "OUT-019"),
|
|
_m("W_SKIP_INTENSITY_ONLY", "warning", "Intensity-only locus was skipped", "OUT-019"),
|
|
_m("W_SKIP_INDEL_CONTEXT", "warning", "Indel sequence cannot be determined (no CSV SourceSeq)", "OUT-019"),
|
|
_m("W_SKIP_INDEL_REF", "warning", "Indel reference allele cannot be determined from the FASTA", "OUT-019"),
|
|
_m("W_INDEL_PARTIAL_MATCH", "warning", "Indel source sequence matches the genome only partially", "OUT-019"),
|
|
_m("W_SKIP_EXCLUDED", "warning", "Locus excluded by --exclude-loci/--include-loci", "OUT-019"),
|
|
_m("W_SKIP_CONTIG", "warning", "Manifest chromosome absent from the FASTA", "OUT-019"),
|
|
_m("W_SKIP_NO_STRAND", "warning", "Locus has neither RefStrand nor a sequence to align", "OUT-042"),
|
|
_m("W_SKIP_AMBIGUOUS", "warning", "Ambiguous-strand locus dropped by --drop-ambiguous", "OUT-042"),
|
|
_m("W_REF_NOT_QUERIED", "warning", "The reference allele is not among the probe's alleles", "OUT-015"),
|
|
_m("W_MERGE_INCONSISTENT", "warning", "Probes at a site disagree; genotype set missing", "OUT-031"),
|
|
_m("W_MERGE_AMBIGUOUS", "warning", "Probes at a site allow several genotypes; set missing", "OUT-031"),
|
|
_m("W_MERGE_ALL_NC", "warning", "Every probe at a merged site is a no-call", "OUT-031"),
|
|
_m("W_INF1_NC_CONFLICT", "warning", "Infinium I no-call conflicts with other assays", "OUT-037"),
|
|
_m("W_NO_BGZIP", "warning", "Uncompressed VCF written; downstream commands need bgzip + index", "OUT-010"),
|
|
_m("W_UNKNOWN_LOCI", "warning", "Names in a locus list are not in the manifest", "OUT-036"),
|
|
_m("E_AUX_INVALID", "error", "Auxiliary-loci record is invalid", "OUT-035"),
|
|
_m("W_LARGE_OUTPUT", "warning", "Output will be very large", "EDGE-036"),
|
|
# --- QC (QC-001 .. QC-064) -----------------------------------------------------------------
|
|
_m("W_CTRL_MISSING", "warning", "Controls needed by a metric are absent; metric not evaluable", "QC-007"),
|
|
_m("W_CTRL_ZERO_DIVISOR", "warning", "Control metric has a zero denominator (NaN)", "QC-008"),
|
|
_m("W_CTRL_DEFS_DIFFER", "warning", "BPM and CSV control definitions differ; the BPM wins", "QC-001"),
|
|
_m("E_QC_NO_DATASETS", "error", "No QC dataset folder found", "QC-041"),
|
|
_m("E_QC_DATASET_INCOMPLETE", "error", "Dataset folder lacks a required file", "QC-041"),
|
|
_m("E_QC_LABEL_COUNT", "error", "Number of labels does not match number of datasets", "QC-042"),
|
|
_m("E_QC_LABEL_DUPLICATE", "error", "Dataset labels are not unique", "QC-042"),
|
|
_m("W_QC_LABEL_SANITIZED", "warning", "Dataset label was sanitized", "QC-042"),
|
|
_m("E_QC_MIXED_TYPES", "error", "Genotyping and methylation datasets cannot be mixed", "QC-043"),
|
|
_m("W_QC_UNMATCHED_SAMPLES", "warning", "Sample IDs present in one QC file but not the other", "QC-040"),
|
|
_m("E_QC_FORMAT", "error", "Invalid --output-format value", "QC-063"),
|
|
_m("E_QC_CONFIG_MISSING", "error", "QC config file does not exist", "QC-021"),
|
|
_m("E_QC_CONFIG_EXT", "error", "QC config file must be .yaml or .yml", "QC-021"),
|
|
_m("E_QC_CONFIG_PARSE", "error", "QC config file is not valid YAML", "QC-021"),
|
|
_m("E_QC_CONFIG_STRUCTURE", "error", "QC config lacks the qc/report (QC/Report) wrapper", "QC-021"),
|
|
_m("E_QC_THRESHOLD_KEY", "error", "Unknown QC threshold key", "QC-021"),
|
|
_m("E_QC_THRESHOLD_VALUE", "error", "QC threshold value is not numeric or null", "QC-021"),
|
|
_m("E_QC_THRESHOLD_RANGE", "error", "QC threshold value out of range", "QC-021"),
|
|
_m("E_QC_PROFILE", "error", "Unknown QC profile", "QC-024"),
|
|
_m("W_QC_PEDIGREE", "warning", "Parent1/Parent2/Replicate value does not name a sample in the set", "IN-018"),
|
|
_m("W_QC_NO_BAF", "warning", "GTC has no B-allele frequencies; contamination not estimated", "QC-064"),
|
|
_m("W_QC_TOO_FEW", "warning", "Too few samples or sites for a cohort QC statistic", "QC-026"),
|
|
# --- cytogenetics CNV/LOH (CYTO-001 .. CYTO-040) -------------------------------------------------
|
|
_m("E_MODEL_MANIFEST_MISMATCH", "error", "Model was built for a different manifest probe set", "IN-051"),
|
|
_m("E_MODEL_VERSION", "error", "Model schema is too old; rebuild it", "IN-051"),
|
|
_m("W_CN_PROBE_FLOOR", "warning", "Minimum probe count below 10 raised to 10", "CYTO-008"),
|
|
_m(
|
|
"W_CN_THRESHOLD_EXCEEDS_GENOME",
|
|
"warning",
|
|
"Size threshold exceeds the effective genome; no events",
|
|
"CYTO-009",
|
|
),
|
|
_m("I_CN_SEX_RESOLVED", "info", "Sample sex updated from U by the caller", "CYTO-024"),
|
|
_m("E_CN_SEX_UNRESOLVED", "error", "Sample sex unresolved and --unknown-sex-policy error", "CYTO-024"),
|
|
_m("W_CN_NO_FASTA", "warning", "No FASTA: VCF REF padding bases written as N", "CYTO-029"),
|
|
_m("E_CN_SIDECAR_MISSING", "error", "cn_call_manifest.json not found in the cn call output", "CYTO-032"),
|
|
_m("E_CN_VCF_UNINDEXED", "error", "CNV VCFs must be bgzipped and indexed for annotate", "CYTO-032"),
|
|
_m("E_CN_FAILED", "error", "CN calling failed for the sample", "CYTO-040"),
|
|
# --- targeted PGx copy number (PCN-001 .. PCN-017) ---------------------------------------------------
|
|
_m("E_PGX_BATCH_TOO_SMALL", "error", "Too few (passing) samples in the batch for PGx CN calling", "PCN-004"),
|
|
_m("W_PGX_BATCH_BELOW_RECOMMENDED", "warning", "Fewer passing samples than recommended", "PCN-004"),
|
|
_m("E_PGX_TRAIN_TOO_SMALL", "error", "Too few (passing) samples to train a PGx CN model", "PCN-005"),
|
|
_m(
|
|
"E_MODEL_CLUSTER_MISMATCH",
|
|
"error",
|
|
"Input GTCs were called with another cluster file than the model",
|
|
"IN-050",
|
|
),
|
|
_m(
|
|
"W_MODEL_CLUSTER_MISMATCH",
|
|
"warning",
|
|
"Input GTCs were called with another cluster file than the model",
|
|
"IN-050",
|
|
),
|
|
_m("W_PGX_REGION_FEW_PROBES", "warning", "PGx CN region has fewer than 10 unmasked probes", "PCN-008"),
|
|
_m("W_PGX_NO_ALT_CONTIG", "warning", "FASTA lacks a contig carrying a PGx region; REF written as N", "IN-062"),
|
|
_m("W_PGX_BATCH_COMPOSITION", "warning", "Batch spans several lots/batches or > 14 days of scans", "PCN-006"),
|
|
_m("E_PGX_CN_FAILED", "error", "PGx CN calling failed for the sample", "PCN-002"),
|
|
_m("E_PGX_SOURCE", "error", "PGx source tables missing; run scripts/pgx/fetch_sources.sh", "PGX-001"),
|
|
# --- methylation control QC (MET-001 .. MET-018) -------------------------------------------------
|
|
_m("E_METH_NO_ASSAY", "error", "Probe section not found", "MET-002"),
|
|
_m("E_METH_NO_CONTROLS", "error", "Missing control probes in manifest", "MET-002"),
|
|
_m("E_METH_MISSING_COLUMNS", "error", "Missing required columns in the methylation manifest", "MET-002"),
|
|
_m(
|
|
"E_METH_CONTROLS_FORMAT",
|
|
"error",
|
|
"Controls not formatted correctly. Must have 4 columns (Address,Type,Color_Channel,Name)",
|
|
"MET-002",
|
|
),
|
|
_m("E_METH_NO_IDATS", "error", "No IDATs found", "MET-004"),
|
|
_m(
|
|
"W_METH_MISSING_SAMPLE",
|
|
"warning",
|
|
"Missing sample: IDATs absent; the sample fails, the run continues",
|
|
"MET-015",
|
|
),
|
|
_m("E_METH_PREP_FAILED", "error", "Prep failed (sample)", "MET-010"),
|
|
_m(
|
|
"W_METH_MISSING_CONTROL_TYPES",
|
|
"warning",
|
|
"Control types missing from the manifest; metric set to NA",
|
|
"MET-009",
|
|
),
|
|
_m(
|
|
"W_METH_MISSING_CONTROL_NAMES",
|
|
"warning",
|
|
"Control names missing from the manifest; metric set to NA",
|
|
"MET-009",
|
|
),
|
|
_m("W_METH_NO_FEATURES", "warning", "No features, skipping PCA plot", "MET-013"),
|
|
]
|
|
)
|
|
|
|
|
|
class SimurgError(Exception):
|
|
"""An error with a stable catalogue code and an exit code."""
|
|
|
|
def __init__(
|
|
self,
|
|
code: str,
|
|
message: str,
|
|
*,
|
|
exit_code: ExitCode = ExitCode.USAGE,
|
|
hint: str | None = None,
|
|
path: str | None = None,
|
|
) -> None:
|
|
if code not in CATALOGUE:
|
|
raise ValueError(f"unknown message code {code!r}")
|
|
self.code = code
|
|
self.message = message
|
|
self.exit_code = exit_code
|
|
self.hint = hint
|
|
self.path = path
|
|
super().__init__(f"[{code}] {message}" + (f" (hint: {hint})" if hint else ""))
|
|
|
|
|
|
class FormatError(SimurgError):
|
|
"""Malformed binary/text input. ``code`` names the file type (E_IDAT_FORMAT, E_GTC_CORRUPT, ...)."""
|
|
|
|
|
|
def render_markdown() -> str:
|
|
"""Render the catalogue as ``docs/messages.md``."""
|
|
lines = [
|
|
"# SimurgArray message catalogue",
|
|
"",
|
|
"Generated from `src/simurg/core/errors.py` — do not edit by hand.",
|
|
"",
|
|
"| Code | Level | Meaning | Spec |",
|
|
"|---|---|---|---|",
|
|
]
|
|
for d in sorted(CATALOGUE.values(), key=lambda m: m.code):
|
|
lines.append(f"| `{d.code}` | {d.level} | {d.summary} | {d.spec} |")
|
|
return "\n".join(lines) + "\n"
|
|
|
|
|
|
if __name__ == "__main__": # pragma: no cover
|
|
print(render_markdown(), end="")
|