From 557cb4c518bc434402db30f466daddc633dc1c5a Mon Sep 17 00:00:00 2001 From: Manuel Holtgrewe Date: Wed, 9 Mar 2022 18:16:18 +0100 Subject: [PATCH] REST API with JSON query generation shortcut (#367) Closes: #367 Related-Issue: #367 Projected-Results-Impact: require-revalidation --- HISTORY.rst | 12 + docs_manual/api_case.rst | 2 + variants/query_presets.py | 1067 ++++++++++++++++ variants/query_schemas.py | 1 - variants/schemas/case-query-v1.json | 5 +- variants/serializers.py | 24 + variants/tests/test_query_presets.py | 1705 ++++++++++++++++++++++++++ variants/tests/test_views_api.py | 176 +++ variants/urls.py | 5 + variants/views_api.py | 176 ++- 10 files changed, 3161 insertions(+), 12 deletions(-) create mode 100644 variants/query_presets.py create mode 100644 variants/tests/test_query_presets.py diff --git a/HISTORY.rst b/HISTORY.rst index 91d8274de..7347c6b5b 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -2,6 +2,18 @@ History / Changelog =================== +----------------- +HEAD (unreleased) +----------------- + +End-User Summary +================ + +Full Change List +================ + +- Adding REST API for generating query shortcuts (#367) + ------ v1.1.0 ------ diff --git a/docs_manual/api_case.rst b/docs_manual/api_case.rst index 578ff534c..c3b9e0c2c 100644 --- a/docs_manual/api_case.rst +++ b/docs_manual/api_case.rst @@ -60,3 +60,5 @@ API Views .. autoclass:: SmallVariantQueryUpdateApiView .. autoclass:: SmallVariantQueryFetchResultsApiView + +.. autoclass:: SmallVariantQuerySettingsShortcutApiView diff --git a/variants/query_presets.py b/variants/query_presets.py new file mode 100644 index 000000000..c9493297c --- /dev/null +++ b/variants/query_presets.py @@ -0,0 +1,1067 @@ +"""Presets for small variant query configuration + +The presets are organized on three levels + +- Top level presets in ``QUICK_PRESETS`` select the ``_QuickPresets`` +- Each `_QuickPresets` defines per category presets in each of the categories +- For each category, there is a `_CategoryPresets` type for the corresponding + ``CATEGORY_PRESETS`` constant that defines the presets +""" + +from enum import unique, Enum +import itertools +import typing + +import attrs + + +@unique +class Sex(Enum): + """Represents the sex in a ``PedigreeMember``.""" + + UNKNOWN = 0 + MALE = 1 + FEMALE = 2 + + +@unique +class DiseaseState(Enum): + """Represents the disease status in a ``PedigreeMember``""" + + UNKNOWN = 0 + UNAFFECTED = 1 + AFFECTED = 2 + + +#: Value to code for "no father/mother" +NOBODY = "0" + + +@attrs.frozen +class PedigreeMember: + """Member in a pedigree""" + + family: typing.Optional[str] + name: str + father: str + mother: str + sex: Sex + disease_state: DiseaseState + + def has_both_parents(self): + return self.father != NOBODY and self.mother != NOBODY + + def is_singleton(self): + return self.father == NOBODY and self.mother == NOBODY + + def has_single_parent(self): + return not self.has_both_parents() and not self.is_singleton() + + def is_affected(self): + return self.disease_state == DiseaseState.AFFECTED + + +@unique +class GenotypeChoice(Enum): + ANY = "any" + REF = "ref" + HET = "het" + HOM = "hom" + NON_HOM = "non-hom" + VARIANT = "variant" + NON_VARIANT = "non-variant" + NON_REFERENCE = "non-reference" + + +@unique +class Inheritance(Enum): + """Preset options for category inheritance""" + + DOMINANT = "dominant" + HOMOZYGOUS_RECESSIVE = "homozygous_recessive" + COMPOUND_HETEROZYGOUS = "compound_heterozygous" + RECESSIVE = "recessive" + X_RECESSIVE = "x_recessive" + AFFECTED_CARRIERS = "affected_carriers" + CUSTOM = "custom" + ANY = "any" + + def _is_recessive(self): + return self in ( + Inheritance.HOMOZYGOUS_RECESSIVE, + Inheritance.COMPOUND_HETEROZYGOUS, + Inheritance.RECESSIVE, + ) + + def to_settings( + self, samples: typing.Iterable[PedigreeMember], index: typing.Optional[str] = None + ) -> typing.Dict[str, typing.Any]: + """Return settings for the inheritance + + All sample names must be of the same family. + """ + assert len(set(s.family for s in samples)) == 1 + + samples_by_name = {s.name: s for s in samples} + + # Select first affected invididual that has both parents set, fall back to first first affected otherwise + affected_samples = [s for s in samples if s.is_affected()] + index_candidates = list( + itertools.chain( + [s for s in affected_samples if s.has_both_parents()], + [s for s in affected_samples if s.has_single_parent()], + affected_samples, + samples, + ) + ) + + if self._is_recessive(): + # Get index, parents, and others (not index, not parents) individuals + recessive_index = index_candidates[0] + parents = [ + s for s in samples if s.name in (recessive_index.father, recessive_index.mother) + ] + parent_names = {p.name for p in parents} + others = {s for s in affected_samples if s.name != index and s.name not in parent_names} + # Fill ``genotype`` for the index + if self == Inheritance.HOMOZYGOUS_RECESSIVE: + genotype = {recessive_index.name: GenotypeChoice.HOM} + mode = {"recessive_mode": None} + elif self == Inheritance.COMPOUND_HETEROZYGOUS: + genotype = {recessive_index.name: None} + mode = {"recessive_mode": "compound-recessive"} + elif self == Inheritance.RECESSIVE: + genotype = {recessive_index.name: None} + mode = {"recessive_mode": "recessive"} + else: + raise RuntimeError(f"Unexpected recessive mode of inheritance: {self}") + # Fill ``genotype`` for parents and others + if self == Inheritance.HOMOZYGOUS_RECESSIVE: + affected_parents = len([p for p in parents if p.is_affected()]) + for parent in parents: + if parent.is_affected(): + genotype[parent.name] = GenotypeChoice.HOM + else: + if affected_parents > 0: + genotype[parent.name] = GenotypeChoice.REF + else: + genotype[parent.name] = GenotypeChoice.HET + else: + for parent in parents: + genotype[parent.name] = None + for other in others: + genotype[other.name] = GenotypeChoice.ANY + # Compose the dict with recessive index, recessive mode, and genotype + return { + "recessive_index": recessive_index.name, + "genotype": genotype, + **mode, + } + elif self == Inheritance.X_RECESSIVE: + male_index_candidates = list( + itertools.chain( + [s for s in index_candidates if s.sex == Sex.MALE], + [s for s in index_candidates if s.sex == Sex.UNKNOWN], + index_candidates, + samples, + ) + ) + recessive_index = male_index_candidates[0] + index_father = samples_by_name.get(recessive_index.father, None) + index_mother = samples_by_name.get(recessive_index.mother, None) + others = [ + s + for s in samples + if s.name + not in (recessive_index.name, recessive_index.father, recessive_index.mother) + ] + genotype = {recessive_index.name: GenotypeChoice.HOM} + if index_father and index_father.is_affected(): + genotype[recessive_index.father] = GenotypeChoice.HOM + if index_mother: + genotype[recessive_index.mother] = GenotypeChoice.REF + elif index_father and not index_father.is_affected(): + genotype[recessive_index.father] = GenotypeChoice.REF + if index_mother: + genotype[recessive_index.mother] = GenotypeChoice.HET + elif index_mother: # and not index_father + genotype[recessive_index.mother] = GenotypeChoice.ANY + for other in others: + genotype[other.name] = GenotypeChoice.ANY + return { + "recessive_index": recessive_index.name, + "recessive_mode": None, + "genotype": genotype, + } + elif self == Inheritance.AFFECTED_CARRIERS: + return { + "recessive_index": None, + "recessive_mode": None, + "genotype": { + s.name: GenotypeChoice.VARIANT if s.is_affected() else GenotypeChoice.REF + for s in samples + }, + } + elif self == Inheritance.DOMINANT: + return { + "recessive_index": None, + "recessive_mode": None, + "genotype": { + s.name: GenotypeChoice.HET if s.is_affected() else GenotypeChoice.REF + for s in samples + }, + } + elif self == Inheritance.ANY: + return { + "recessive_index": None, + "recessive_mode": None, + "genotype": {s.name: GenotypeChoice.ANY for s in samples}, + } + else: + raise ValueError(f"Cannot generate settings for inheritance {self}") + + +@attrs.frozen +class _FrequencyPresets: + """Type for providing immutable frequency presets""" + + #: Presets for "dominant super strict" frequency + dominant_super_strict: typing.Dict[str, typing.Any] = { + "thousand_genomes_enabled": True, + "thousand_genomes_homozygous": 0, + "thousand_genomes_heterozygous": 1, + "thousand_genomes_hemizygous": None, + "thousand_genomes_frequency": 0.002, + "exac_enabled": True, + "exac_homozygous": 0, + "exac_heterozygous": 1, + "exac_hemizygous": None, + "exac_frequency": 0.002, + "gnomad_exomes_enabled": True, + "gnomad_exomes_homozygous": 0, + "gnomad_exomes_heterozygous": 1, + "gnomad_exomes_hemizygous": None, + "gnomad_exomes_frequency": 0.002, + "gnomad_genomes_enabled": True, + "gnomad_genomes_homozygous": 0, + "gnomad_genomes_heterozygous": 1, + "gnomad_genomes_hemizygous": None, + "gnomad_genomes_frequency": 0.002, + "inhouse_enabled": True, + "inhouse_homozygous": None, + "inhouse_heterozygous": None, + "inhouse_hemizygous": None, + "inhouse_carriers": 20, + "mtdb_enabled": False, + "mtdb_count": None, + "mtdb_frequency": None, + "helixmtdb_enabled": False, + "helixmtdb_het_count": None, + "helixmtdb_hom_count": None, + "helixmtdb_frequency": None, + "mitomap_enabled": False, + "mitomap_count": None, + "mitomap_frequency": None, + } + #: Presets for "dominant strict" frequency + dominant_strict: typing.Dict[str, typing.Any] = { + "thousand_genomes_enabled": True, + "thousand_genomes_homozygous": 0, + "thousand_genomes_heterozygous": 4, + "thousand_genomes_hemizygous": None, + "thousand_genomes_frequency": 0.002, + "exac_enabled": True, + "exac_homozygous": 0, + "exac_heterozygous": 10, + "exac_hemizygous": None, + "exac_frequency": 0.002, + "gnomad_exomes_enabled": True, + "gnomad_exomes_homozygous": 0, + "gnomad_exomes_heterozygous": 20, + "gnomad_exomes_hemizygous": None, + "gnomad_exomes_frequency": 0.002, + "gnomad_genomes_enabled": True, + "gnomad_genomes_homozygous": 0, + "gnomad_genomes_heterozygous": 4, + "gnomad_genomes_hemizygous": None, + "gnomad_genomes_frequency": 0.002, + "inhouse_enabled": True, + "inhouse_homozygous": None, + "inhouse_heterozygous": None, + "inhouse_hemizygous": None, + "inhouse_carriers": 20, + "mtdb_enabled": True, + "mtdb_count": 10, + "mtdb_frequency": 0.01, + "helixmtdb_enabled": True, + "helixmtdb_hom_count": 200, + "helixmtdb_het_count": None, + "helixmtdb_frequency": 0.01, + "mitomap_enabled": False, + "mitomap_count": None, + "mitomap_frequency": None, + } + #: Presets for "dominant relaxed" frequency + dominant_relaxed: typing.Dict[str, typing.Any] = { + "thousand_genomes_enabled": True, + "thousand_genomes_homozygous": 0, + "thousand_genomes_heterozygous": 10, + "thousand_genomes_hemizygous": None, + "thousand_genomes_frequency": 0.01, + "exac_enabled": True, + "exac_homozygous": 0, + "exac_heterozygous": 25, + "exac_hemizygous": None, + "exac_frequency": 0.01, + "gnomad_exomes_enabled": True, + "gnomad_exomes_homozygous": 0, + "gnomad_exomes_heterozygous": 50, + "gnomad_exomes_hemizygous": None, + "gnomad_exomes_frequency": 0.01, + "gnomad_genomes_enabled": True, + "gnomad_genomes_homozygous": 0, + "gnomad_genomes_heterozygous": 20, + "gnomad_genomes_hemizygous": None, + "gnomad_genomes_frequency": 0.01, + "inhouse_enabled": True, + "inhouse_homozygous": None, + "inhouse_heterozygous": None, + "inhouse_hemizygous": None, + "inhouse_carriers": 20, + "mtdb_enabled": True, + "mtdb_count": 50, + "mtdb_frequency": 0.15, + "helixmtdb_enabled": True, + "helixmtdb_het_count": None, + "helixmtdb_hom_count": 400, + "helixmtdb_frequency": 0.15, + "mitomap_enabled": False, + "mitomap_count": None, + "mitomap_frequency": None, + } + #: Presets for "recessive strict" frequency + recessive_strict: typing.Dict[str, typing.Any] = { + "thousand_genomes_enabled": True, + "thousand_genomes_homozygous": 0, + "thousand_genomes_heterozygous": 24, + "thousand_genomes_hemizygous": None, + "thousand_genomes_frequency": 0.001, + "exac_enabled": True, + "exac_homozygous": 0, + "exac_heterozygous": 60, + "exac_hemizygous": None, + "exac_frequency": 0.001, + "gnomad_exomes_enabled": True, + "gnomad_exomes_homozygous": 0, + "gnomad_exomes_heterozygous": 120, + "gnomad_exomes_hemizygous": None, + "gnomad_exomes_frequency": 0.001, + "gnomad_genomes_enabled": True, + "gnomad_genomes_homozygous": 0, + "gnomad_genomes_heterozygous": 15, + "gnomad_genomes_hemizygous": None, + "gnomad_genomes_frequency": 0.001, + "inhouse_enabled": True, + "inhouse_homozygous": None, + "inhouse_heterozygous": None, + "inhouse_hemizygous": None, + "inhouse_carriers": 20, + "mtdb_enabled": False, + "mtdb_count": None, + "mtdb_frequency": None, + "helixmtdb_enabled": False, + "helixmtdb_het_count": None, + "helixmtdb_hom_count": None, + "helixmtdb_frequency": None, + "mitomap_enabled": False, + "mitomap_count": None, + "mitomap_frequency": None, + } + #: Presets for "recessive relaxed" frequency + recessive_relaxed: typing.Dict[str, typing.Any] = { + "thousand_genomes_enabled": True, + "thousand_genomes_homozygous": 4, + "thousand_genomes_heterozygous": 240, + "thousand_genomes_hemizygous": None, + "thousand_genomes_frequency": 0.01, + "exac_enabled": True, + "exac_homozygous": 10, + "exac_heterozygous": 600, + "exac_hemizygous": None, + "exac_frequency": 0.01, + "gnomad_exomes_enabled": True, + "gnomad_exomes_homozygous": 20, + "gnomad_exomes_heterozygous": 1200, + "gnomad_exomes_hemizygous": None, + "gnomad_exomes_frequency": 0.01, + "gnomad_genomes_enabled": True, + "gnomad_genomes_homozygous": 4, + "gnomad_genomes_heterozygous": 150, + "gnomad_genomes_hemizygous": None, + "gnomad_genomes_frequency": 0.01, + "inhouse_enabled": True, + "inhouse_homozygous": None, + "inhouse_heterozygous": None, + "inhouse_hemizygous": None, + "inhouse_carriers": 20, + "mtdb_enabled": False, + "mtdb_count": None, + "mtdb_frequency": None, + "helixmtdb_enabled": False, + "helixmtdb_het_count": None, + "helixmtdb_hom_count": None, + "helixmtdb_frequency": None, + "mitomap_enabled": False, + "mitomap_count": None, + "mitomap_frequency": None, + } + #: Presets for "any" frequency + any: typing.Dict[str, typing.Any] = { + "thousand_genomes_enabled": False, + "thousand_genomes_homozygous": None, + "thousand_genomes_heterozygous": None, + "thousand_genomes_hemizygous": None, + "thousand_genomes_frequency": None, + "exac_enabled": False, + "exac_homozygous": None, + "exac_heterozygous": None, + "exac_hemizygous": None, + "exac_frequency": None, + "gnomad_exomes_enabled": False, + "gnomad_exomes_homozygous": None, + "gnomad_exomes_heterozygous": None, + "gnomad_exomes_hemizygous": None, + "gnomad_exomes_frequency": None, + "gnomad_genomes_enabled": False, + "gnomad_genomes_homozygous": None, + "gnomad_genomes_heterozygous": None, + "gnomad_genomes_hemizygous": None, + "gnomad_genomes_frequency": None, + "inhouse_enabled": False, + "inhouse_homozygous": None, + "inhouse_heterozygous": None, + "inhouse_carriers": None, + "mtdb_enabled": False, + "mtdb_count": None, + "mtdb_frequency": None, + "helixmtdb_enabled": False, + "helixmtdb_het_count": None, + "helixmtdb_hom_count": None, + "helixmtdb_frequency": None, + "mitomap_enabled": False, + "mitomap_count": None, + "mitomap_frequency": None, + } + + +#: Presets for the impact related settings by frequency preset option +FREQUENCY_PRESETS: _FrequencyPresets = _FrequencyPresets() + + +@unique +class Frequency(Enum): + """Preset options for category frequency""" + + DOMINANT_SUPER_STRICT = "dominant_super_strict" + DOMINANT_STRICT = "dominant_strict" + DOMINANT_RELAXED = "dominant_relaxed" + RECESSIVE_STRICT = "recessive_strict" + RECESSIVE_RELAXED = "recessive_relaxed" + CUSTOM = "custom" + ANY = "any" + + def to_settings(self) -> typing.Dict[str, typing.Any]: + """Return settings for the frequency category""" + return getattr(FREQUENCY_PRESETS, self.value) + + +@attrs.frozen +class _ImpactPresets: + """Type for providing immutable impact presets""" + + #: Presets for "null variant" impact + null_variant: typing.Dict[str, typing.Any] = { + "var_type_snv": True, + "var_type_mnv": True, + "var_type_indel": True, + "transcripts_coding": True, + "transcripts_noncoding": False, + "effects": ( + "exon_loss_variant", + "feature_truncation", + "frameshift_elongation", + "frameshift_truncation", + "frameshift_variant", + "internal_feature_elongation", + "splice_acceptor_variant", + "splice_donor_variant", + "start_lost", + "stop_gained", + "stop_lost", + "structural_variant", + "transcript_ablation", + ), + } + #: Presets for "amino acid change and splicing" impact + aa_change_splicing: typing.Dict[str, typing.Any] = { + "var_type_snv": True, + "var_type_mnv": True, + "var_type_indel": True, + "transcripts_coding": True, + "transcripts_noncoding": False, + "effects": ( + "complex_substitution", + "direct_tandem_duplication", + "disruptive_inframe_deletion", + "disruptive_inframe_insertion", + "exon_loss_variant", + "feature_truncation", + "frameshift_elongation", + "frameshift_truncation", + "frameshift_variant", + "inframe_deletion", + "inframe_insertion", + "internal_feature_elongation", + "missense_variant", + "mnv", + "splice_acceptor_variant", + "splice_donor_variant", + "splice_region_variant", + "start_lost", + "stop_gained", + "stop_lost", + "structural_variant", + "transcript_ablation", + ), + } + #: Presets for "all coding and deep intronic" impact + all_coding_deep_intronic: typing.Dict[str, typing.Any] = { + "var_type_snv": True, + "var_type_mnv": True, + "var_type_indel": True, + "transcripts_coding": True, + "transcripts_noncoding": False, + "effects": ( + "coding_transcript_intron_variant", + "complex_substitution", + "direct_tandem_duplication", + "disruptive_inframe_deletion", + "disruptive_inframe_insertion", + "exon_loss_variant", + "feature_truncation", + "frameshift_elongation", + "frameshift_truncation", + "frameshift_variant", + "inframe_deletion", + "inframe_insertion", + "internal_feature_elongation", + "missense_variant", + "mnv", + "splice_acceptor_variant", + "splice_donor_variant", + "splice_region_variant", + "start_lost", + "stop_gained", + "stop_lost", + "structural_variant", + "synonymous_variant", + "transcript_ablation", + ), + } + #: Presets for "whole transcript" impact + whole_transcript: typing.Dict[str, typing.Any] = { + "var_type_snv": True, + "var_type_mnv": True, + "var_type_indel": True, + "transcripts_coding": True, + "transcripts_noncoding": False, + "effects": ( + "coding_transcript_intron_variant", + "complex_substitution", + "direct_tandem_duplication", + "disruptive_inframe_deletion", + "disruptive_inframe_insertion", + "exon_loss_variant", + "feature_truncation", + "five_prime_UTR_exon_variant", + "five_prime_UTR_intron_variant", + "frameshift_elongation", + "frameshift_truncation", + "frameshift_variant", + "inframe_deletion", + "inframe_insertion", + "internal_feature_elongation", + "missense_variant", + "mnv", + "splice_acceptor_variant", + "splice_donor_variant", + "splice_region_variant", + "start_lost", + "stop_gained", + "stop_lost", + "stop_retained_variant", + "structural_variant", + "synonymous_variant", + "three_prime_UTR_exon_variant", + "three_prime_UTR_intron_variant", + "transcript_ablation", + ), + } + #: Presets for "any" impact + any: typing.Dict[str, typing.Any] = { + "var_type_snv": True, + "var_type_mnv": True, + "var_type_indel": True, + "transcripts_coding": True, + "transcripts_noncoding": True, + "effects": ( + "coding_transcript_intron_variant", + "complex_substitution", + "direct_tandem_duplication", + "disruptive_inframe_deletion", + "disruptive_inframe_insertion", + "downstream_gene_variant", + "exon_loss_variant", + "feature_truncation", + "five_prime_UTR_exon_variant", + "five_prime_UTR_intron_variant", + "frameshift_elongation", + "frameshift_truncation", + "frameshift_variant", + "inframe_deletion", + "inframe_insertion", + "intergenic_variant", + "internal_feature_elongation", + "missense_variant", + "mnv", + "non_coding_transcript_exon_variant", + "non_coding_transcript_intron_variant", + "splice_acceptor_variant", + "splice_donor_variant", + "splice_region_variant", + "start_lost", + "stop_gained", + "stop_lost", + "stop_retained_variant", + "structural_variant", + "synonymous_variant", + "three_prime_UTR_exon_variant", + "three_prime_UTR_intron_variant", + "transcript_ablation", + "upstream_gene_variant", + ), + } + + +#: Presets for the impact related settings by impact preset option +IMPACT_PRESETS: _ImpactPresets = _ImpactPresets() + + +@unique +class Impact(Enum): + """Preset options for category impact""" + + NULL_VARIANT = "null_variant" + AA_CHANGE_SPLICING = "aa_change_splicing" + ALL_CODING_DEEP_INTRONIC = "all_coding_deep_intronic" + WHOLE_TRANSCRIPT = "whole_transcript" + CUSTOM = "custom" + ANY = "any" + + def to_settings(self) -> typing.Dict[str, typing.Any]: + """Return settings for the impact category""" + return getattr(IMPACT_PRESETS, self.value) + + +@unique +class _QualityFail(Enum): + """Enum for representing the action on variant call quality fail""" + + #: Drop variant + DROP_VARIANT = "drop-variant" + #: Ignore failure + IGNORE = "ignore" + #: Interpret as "no-call" + NO_CALL = "no-call" + + +@attrs.frozen +class _QualityPresets: + """Type for providing immutable quality presets""" + + #: Presets for "super strict" quality settings + super_strict: typing.Dict[str, typing.Any] = { + "dp_het": 10, + "dp_hom": 5, + "ab": 0.3, + "gq": 30, + "ad": 3, + "ad_max": None, + "fail": _QualityFail.DROP_VARIANT.value, + } + #: Presets for the "strict" quality settings + strict: typing.Dict[str, typing.Any] = { + "dp_het": 10, + "dp_hom": 5, + "ab": 0.2, + "gq": 10, + "ad": 3, + "ad_max": None, + "fail": _QualityFail.DROP_VARIANT.value, + } + #: Presets for the "relaxed" quality settings + relaxed: typing.Dict[str, typing.Any] = { + "dp_het": 8, + "dp_hom": 4, + "ab": 0.1, + "gq": 10, + "ad": 2, + "ad_max": None, + "fail": _QualityFail.DROP_VARIANT.value, + } + #: Presets for the "any" quality settings + any: typing.Dict[str, typing.Any] = { + "dp_het": 0, + "dp_hom": 0, + "ab": 0.0, + "gq": 0, + "ad": 0, + "ad_max": None, + "fail": _QualityFail.IGNORE.value, + } + + +#: Presets for the quality related settings, by quality preset option +QUALITY_PRESETS: _QualityPresets = _QualityPresets() + + +@unique +class Quality(Enum): + """Preset options for category quality""" + + SUPER_STRICT = "super_strict" + STRICT = "strict" + RELAXED = "relaxed" + CUSTOM = "custom" + ANY = "any" + + def to_settings(self, samples: typing.Iterable[PedigreeMember]) -> typing.Dict[str, typing.Any]: + """Return settings for the quality category + + Returns a ``dict`` with the key ``quality`` that contains one entry for each sample from ``sample_names`` + that contains the quality filter settings for the given sample. + + All sample names must be of the same family. + """ + assert len(set(s.family for s in samples)) == 1 + return {sample.name: dict(getattr(QUALITY_PRESETS, self.value)) for sample in samples} + + +@attrs.frozen +class _ChromosomePresets: + """Type for providing immutable chromosome/region/gene presets""" + + #: Presets for the "whole genome" chromosome/region/gene settings + whole_genome: typing.Dict[str, typing.Any] = { + "genomic_region": (), + "gene_allowlist": (), + "gene_blocklist": (), + } + #: Presets for the "autosomes" chromosome/region/gene settings + autosomes: typing.Dict[str, typing.Any] = { + "genomic_region": tuple(f"chr{num}" for num in range(1, 23)), + "gene_allowlist": (), + "gene_blocklist": (), + } + #: Presets for the "X-chromosome" chromosome/region/gene settings + x_chromosome: typing.Dict[str, typing.Any] = { + "genomic_region": ("chrX",), + "gene_allowlist": (), + "gene_blocklist": (), + } + #: Presets for the "Y-chromosomes" chromosome/region/gene settings + y_chromosome: typing.Dict[str, typing.Any] = { + "genomic_region": ("chrY",), + "gene_allowlist": (), + "gene_blocklist": (), + } + #: Presets for the "mitochondrial" chromosome/region/gene settings + mt_chromosome: typing.Dict[str, typing.Any] = { + "genomic_region": ("chrMT",), + "gene_allowlist": (), + "gene_blocklist": (), + } + + +#: Presets for the chromosome/region/gene related settings, by chromosome preset option +CHROMOSOME_PRESETS: _ChromosomePresets = _ChromosomePresets() + + +@unique +class Chromosomes(Enum): + """Presets options for category chromosomes""" + + WHOLE_GENOME = "whole_genome" + AUTOSOMES = "autosomes" + X_CHROMOSOME = "x_chromosome" + Y_CHROMOSOME = "y_chromosome" + MT_CHROMOSOME = "mt_chromosome" + CUSTOM = "custom" + + def to_settings(self) -> typing.Dict[str, typing.Any]: + """Return settings for this flags etc. category""" + return getattr(CHROMOSOME_PRESETS, self.value) + + +@attrs.frozen +class _FlagsEtcPresets: + """Type for providing immutable chromosome/region/gene presets""" + + #: Presets for the "defaults" flags etc. settings + defaults: typing.Dict[str, typing.Any] = { + "clinvar_include_benign": False, + "clinvar_include_likely_benign": False, + "clinvar_include_likely_pathogenic": True, + "clinvar_include_pathogenic": True, + "clinvar_include_uncertain_significance": False, + "flag_bookmarked": True, + "flag_candidate": True, + "flag_doesnt_segregate": True, + "flag_final_causative": True, + "flag_for_validation": True, + "flag_no_disease_association": True, + "flag_phenotype_match_empty": True, + "flag_phenotype_match_negative": True, + "flag_phenotype_match_positive": True, + "flag_phenotype_match_uncertain": True, + "flag_segregates": True, + "flag_simple_empty": True, + "flag_summary_empty": True, + "flag_summary_negative": True, + "flag_summary_positive": True, + "flag_summary_uncertain": True, + "flag_validation_empty": True, + "flag_validation_negative": True, + "flag_validation_positive": True, + "flag_validation_uncertain": True, + "flag_visual_empty": True, + "flag_visual_negative": True, + "flag_visual_positive": True, + "flag_visual_uncertain": True, + "remove_if_in_dbsnp": False, + "require_in_clinvar": False, + "require_in_hgmd_public": False, + } + #: Presets for the "Clinvar only" flags etc. settings + clinvar_only: typing.Dict[str, typing.Any] = { + "flag_bookmarked": True, + "flag_candidate": True, + "flag_doesnt_segregate": True, + "flag_final_causative": True, + "flag_for_validation": True, + "flag_no_disease_association": True, + "flag_phenotype_match_empty": True, + "flag_phenotype_match_negative": True, + "flag_phenotype_match_positive": True, + "flag_phenotype_match_uncertain": True, + "flag_segregates": True, + "flag_simple_empty": True, + "flag_summary_empty": True, + "flag_summary_negative": True, + "flag_summary_positive": True, + "flag_summary_uncertain": True, + "flag_validation_empty": True, + "flag_validation_negative": True, + "flag_validation_positive": True, + "flag_validation_uncertain": True, + "flag_visual_empty": True, + "flag_visual_negative": True, + "flag_visual_positive": True, + "flag_visual_uncertain": True, + "remove_if_in_dbsnp": False, + "require_in_clinvar": True, + "require_in_hgmd_public": False, + } + #: Presets for the "user flagged" flags etc. settings + user_flagged: typing.Dict[str, typing.Any] = { + "clinvar_include_benign": False, + "clinvar_include_likely_benign": False, + "clinvar_include_likely_pathogenic": True, + "clinvar_include_pathogenic": True, + "clinvar_include_uncertain_significance": False, + "flag_bookmarked": True, + "flag_candidate": True, + "flag_doesnt_segregate": True, + "flag_final_causative": True, + "flag_for_validation": True, + "flag_no_disease_association": True, + "flag_phenotype_match_empty": False, + "flag_phenotype_match_negative": True, + "flag_phenotype_match_positive": True, + "flag_phenotype_match_uncertain": True, + "flag_segregates": True, + "flag_simple_empty": False, + "flag_summary_empty": False, + "flag_summary_negative": True, + "flag_summary_positive": True, + "flag_summary_uncertain": True, + "flag_validation_empty": False, + "flag_validation_negative": True, + "flag_validation_positive": True, + "flag_validation_uncertain": True, + "flag_visual_empty": False, + "flag_visual_negative": True, + "flag_visual_positive": True, + "flag_visual_uncertain": True, + "remove_if_in_dbsnp": False, + "require_in_clinvar": False, + "require_in_hgmd_public": False, + } + + +#: Presets for the chromosome/region/gene related settings, by chromosome preset option +FLAGS_ETC_PRESETS: _FlagsEtcPresets = _FlagsEtcPresets() + + +@unique +class FlagsEtc(Enum): + """Preset options for category flags etc.""" + + DEFAULTS = "defaults" + CLINVAR_ONLY = "clinvar_only" + USER_FLAGGED = "user_flagged" + CUSTOM = "custom" + + def to_settings(self) -> typing.Dict[str, typing.Any]: + """Return settings for this flags etc. category""" + return getattr(FLAGS_ETC_PRESETS, self.value) + + +@attrs.frozen +class QuickPresets: + """Type for the global quick presets""" + + #: presets in category inheritance + inheritance: Inheritance + #: presets in category frequency + frequency: Frequency + #: presets in category impact + impact: Impact + #: presets in category quality + quality: Quality + #: presets in category chromosomes + chromosomes: Chromosomes + #: presets in category flags etc. + flags_etc: FlagsEtc + + def to_settings(self, samples: typing.Iterable[PedigreeMember]) -> typing.Dict[str, typing.Any]: + """Return the overall settings given the case names""" + assert len(set(s.family for s in samples)) == 1 + return { + **self.inheritance.to_settings(samples), + **self.frequency.to_settings(), + **self.impact.to_settings(), + **self.quality.to_settings(samples), + **self.chromosomes.to_settings(), + **self.flags_etc.to_settings(), + } + + +@attrs.frozen +class _QuickPresetList: + """Type for the top-level quick preset list""" + + #: default presets + defaults: QuickPresets = QuickPresets( + inheritance=Inheritance.ANY, + frequency=Frequency.DOMINANT_STRICT, + impact=Impact.AA_CHANGE_SPLICING, + quality=Quality.STRICT, + chromosomes=Chromosomes.WHOLE_GENOME, + flags_etc=FlagsEtc.DEFAULTS, + ) + #: *de novo* presets + de_novo: QuickPresets = QuickPresets( + inheritance=Inheritance.DOMINANT, + frequency=Frequency.DOMINANT_STRICT, + impact=Impact.AA_CHANGE_SPLICING, + quality=Quality.RELAXED, + chromosomes=Chromosomes.WHOLE_GENOME, + flags_etc=FlagsEtc.DEFAULTS, + ) + #: dominant presets + dominant: QuickPresets = QuickPresets( + inheritance=Inheritance.DOMINANT, + frequency=Frequency.DOMINANT_STRICT, + impact=Impact.AA_CHANGE_SPLICING, + quality=Quality.STRICT, + chromosomes=Chromosomes.WHOLE_GENOME, + flags_etc=FlagsEtc.DEFAULTS, + ) + #: homozygous recessive presets + homozygous_recessive: QuickPresets = QuickPresets( + inheritance=Inheritance.HOMOZYGOUS_RECESSIVE, + frequency=Frequency.RECESSIVE_STRICT, + impact=Impact.AA_CHANGE_SPLICING, + quality=Quality.STRICT, + chromosomes=Chromosomes.WHOLE_GENOME, + flags_etc=FlagsEtc.DEFAULTS, + ) + #: compound recessive recessive presets + compound_recessive: QuickPresets = QuickPresets( + inheritance=Inheritance.COMPOUND_HETEROZYGOUS, + frequency=Frequency.RECESSIVE_STRICT, + impact=Impact.AA_CHANGE_SPLICING, + quality=Quality.STRICT, + chromosomes=Chromosomes.WHOLE_GENOME, + flags_etc=FlagsEtc.DEFAULTS, + ) + #: recessive presets + recessive: QuickPresets = QuickPresets( + inheritance=Inheritance.RECESSIVE, + frequency=Frequency.RECESSIVE_STRICT, + impact=Impact.AA_CHANGE_SPLICING, + quality=Quality.STRICT, + chromosomes=Chromosomes.WHOLE_GENOME, + flags_etc=FlagsEtc.DEFAULTS, + ) + #: X-recessive presets + x_recessive: QuickPresets = QuickPresets( + inheritance=Inheritance.X_RECESSIVE, + frequency=Frequency.RECESSIVE_STRICT, + impact=Impact.AA_CHANGE_SPLICING, + quality=Quality.STRICT, + chromosomes=Chromosomes.X_CHROMOSOME, + flags_etc=FlagsEtc.DEFAULTS, + ) + #: Clinvar pathogenic presets + clinvar_pathogenic: QuickPresets = QuickPresets( + inheritance=Inheritance.AFFECTED_CARRIERS, + frequency=Frequency.RECESSIVE_RELAXED, + impact=Impact.ANY, + quality=Quality.STRICT, + chromosomes=Chromosomes.WHOLE_GENOME, + flags_etc=FlagsEtc.CLINVAR_ONLY, + ) + #: mitochondrial recessive presets + mitochondrial: QuickPresets = QuickPresets( + inheritance=Inheritance.AFFECTED_CARRIERS, + frequency=Frequency.DOMINANT_STRICT, + impact=Impact.ANY, + quality=Quality.STRICT, + chromosomes=Chromosomes.MT_CHROMOSOME, + flags_etc=FlagsEtc.DEFAULTS, + ) + #: whole exome recessive presets + whole_exome: QuickPresets = QuickPresets( + inheritance=Inheritance.ANY, + frequency=Frequency.ANY, + impact=Impact.ANY, + quality=Quality.ANY, + chromosomes=Chromosomes.WHOLE_GENOME, + flags_etc=FlagsEtc.DEFAULTS, + ) + + +#: Top level quick presets. +QUICK_PRESETS: _QuickPresetList = _QuickPresetList() diff --git a/variants/query_schemas.py b/variants/query_schemas.py index 0b914d4c3..45ca5cab8 100644 --- a/variants/query_schemas.py +++ b/variants/query_schemas.py @@ -102,7 +102,6 @@ class GenotypeChoiceV1(Enum): HET = "het" HOM = "hom" NON_HOM = "non-hom" - REFERENCE = "reference" VARIANT = "variant" NON_VARIANT = "non-variant" NON_REFERENCE = "non-reference" diff --git a/variants/schemas/case-query-v1.json b/variants/schemas/case-query-v1.json index 72c852f22..561986836 100644 --- a/variants/schemas/case-query-v1.json +++ b/variants/schemas/case-query-v1.json @@ -1183,8 +1183,8 @@ "SAMPLE": "hom" }, { - "FATHER": "reference", - "MOTHER": "reference", + "FATHER": "ref", + "MOTHER": "ref", "CHILD": "het" } ], @@ -1198,7 +1198,6 @@ "het", "hom", "non-hom", - "reference", "variant", "non-variant", "non-reference" diff --git a/variants/serializers.py b/variants/serializers.py index 919aa37ac..5f7a840d9 100644 --- a/variants/serializers.py +++ b/variants/serializers.py @@ -2,7 +2,9 @@ # TODO: rename pedigree entry field "patient" also internally to name and get rid of translation below import copy +import typing +import attrs from bgjobs.models import BackgroundJob from django.db import transaction from django.db.models import Q @@ -324,3 +326,25 @@ class Meta: "ensembl_effect", "ensembl_exon_dist", ) + + +@attrs.define +class SettingsShortcuts: + """Helper class that contains the results of the settings shortcuts""" + + presets: typing.Dict[str, str] + query_settings: typing.Dict[str, typing.Any] + + +class SettingsShortcutsSerializer(serializers.Serializer): + """Serializer for ``SettingsShortcut``""" + + presets = serializers.JSONField() + query_settings = serializers.JSONField() + + class Meta: + model = SettingsShortcuts + fields = ( + "presets", + "query_settings", + ) diff --git a/variants/tests/test_query_presets.py b/variants/tests/test_query_presets.py new file mode 100644 index 000000000..25bf81814 --- /dev/null +++ b/variants/tests/test_query_presets.py @@ -0,0 +1,1705 @@ +"""Tests for the ``variants.query_presets`` module.""" + +from test_plus.test import TestCase + +from variants import query_presets + + +class TestEnumSex(TestCase): + def testValues(self): + self.assertEqual(query_presets.Sex.UNKNOWN.value, 0) + self.assertEqual(query_presets.Sex.MALE.value, 1) + self.assertEqual(query_presets.Sex.FEMALE.value, 2) + + +class TestEnumDiseaseState(TestCase): + def testValues(self): + self.assertEqual(query_presets.DiseaseState.UNKNOWN.value, 0) + self.assertEqual(query_presets.DiseaseState.UNAFFECTED.value, 1) + self.assertEqual(query_presets.DiseaseState.AFFECTED.value, 2) + + +class TestConstantNobody(TestCase): + def testValue(self): + self.assertEqual(query_presets.NOBODY, "0") + + +class TestPedigreeMember(TestCase): + def testConstructor(self): + member = query_presets.PedigreeMember( + family="FAM", + name="index", + father="father", + mother="mother", + sex=query_presets.Sex.MALE, + disease_state=query_presets.DiseaseState.AFFECTED, + ) + self.assertEqual( + str(member), + "PedigreeMember(family='FAM', name='index', father='father', mother='mother', " + "sex=, disease_state=)", + ) + + def testFunctionsSingleton(self): + member = query_presets.PedigreeMember( + family="FAM", + name="index", + father="father", + mother="mother", + sex=query_presets.Sex.MALE, + disease_state=query_presets.DiseaseState.AFFECTED, + ) + self.assertTrue(member.has_both_parents()) + self.assertFalse(member.is_singleton()) + self.assertFalse(member.has_single_parent()) + + def testFunctionsChildFather(self): + child = query_presets.PedigreeMember( + family="FAM", + name="index", + father="father", + mother=query_presets.NOBODY, + sex=query_presets.Sex.MALE, + disease_state=query_presets.DiseaseState.AFFECTED, + ) + self.assertFalse(child.has_both_parents()) + self.assertFalse(child.is_singleton()) + self.assertTrue(child.has_single_parent()) + + def testFunctionsChildMother(self): + child = query_presets.PedigreeMember( + family="FAM", + name="index", + father=query_presets.NOBODY, + mother="mother", + sex=query_presets.Sex.MALE, + disease_state=query_presets.DiseaseState.AFFECTED, + ) + self.assertFalse(child.has_both_parents()) + self.assertFalse(child.is_singleton()) + self.assertTrue(child.has_single_parent()) + + def testFunctionsTrio(self): + child = query_presets.PedigreeMember( + family="FAM", + name="index", + father="father", + mother="mother", + sex=query_presets.Sex.MALE, + disease_state=query_presets.DiseaseState.AFFECTED, + ) + _father = query_presets.PedigreeMember( + family="FAM", + name="index", + father="father", + mother="mother", + sex=query_presets.Sex.MALE, + disease_state=query_presets.DiseaseState.AFFECTED, + ) + _mother = query_presets.PedigreeMember( + family="FAM", + name="index", + father="father", + mother="mother", + sex=query_presets.Sex.MALE, + disease_state=query_presets.DiseaseState.AFFECTED, + ) + self.assertTrue(child.has_both_parents()) + self.assertFalse(child.is_singleton()) + self.assertFalse(child.has_single_parent()) + + +class TestEnumGenotypeChoice(TestCase): + def testValues(self): + self.assertEqual(query_presets.GenotypeChoice.ANY.value, "any") + self.assertEqual(query_presets.GenotypeChoice.REF.value, "ref") + self.assertEqual(query_presets.GenotypeChoice.HET.value, "het") + self.assertEqual(query_presets.GenotypeChoice.HOM.value, "hom") + self.assertEqual(query_presets.GenotypeChoice.NON_HOM.value, "non-hom") + self.assertEqual(query_presets.GenotypeChoice.VARIANT.value, "variant") + self.assertEqual(query_presets.GenotypeChoice.NON_VARIANT.value, "non-variant") + self.assertEqual(query_presets.GenotypeChoice.NON_REFERENCE.value, "non-reference") + + +class PedigreesMixin: + def setUp(self): + super().setUp() + self.maxDiff = None + # Setup some pedigrees for testing + self.singleton = ( + query_presets.PedigreeMember( + family="FAM", + name="index", + father=query_presets.NOBODY, + mother=query_presets.NOBODY, + sex=query_presets.Sex.MALE, + disease_state=query_presets.DiseaseState.AFFECTED, + ), + ) + self.child_father = ( + query_presets.PedigreeMember( + family="FAM", + name="index", + father="father", + mother=query_presets.NOBODY, + sex=query_presets.Sex.MALE, + disease_state=query_presets.DiseaseState.AFFECTED, + ), + query_presets.PedigreeMember( + family="FAM", + name="father", + father=query_presets.NOBODY, + mother=query_presets.NOBODY, + sex=query_presets.Sex.MALE, + disease_state=query_presets.DiseaseState.AFFECTED, + ), + ) + self.child_mother = ( + query_presets.PedigreeMember( + family="FAM", + name="index", + father=query_presets.NOBODY, + mother="mother", + sex=query_presets.Sex.MALE, + disease_state=query_presets.DiseaseState.AFFECTED, + ), + query_presets.PedigreeMember( + family="FAM", + name="mother", + father=query_presets.NOBODY, + mother=query_presets.NOBODY, + sex=query_presets.Sex.FEMALE, + disease_state=query_presets.DiseaseState.AFFECTED, + ), + ) + # Trio compatible with denovo mode of inheritance (also compatible with recessive) + self.trio_denovo = ( + query_presets.PedigreeMember( + family="FAM", + name="index", + father="father", + mother="mother", + sex=query_presets.Sex.MALE, + disease_state=query_presets.DiseaseState.AFFECTED, + ), + query_presets.PedigreeMember( + family="FAM", + name="father", + father=query_presets.NOBODY, + mother=query_presets.NOBODY, + sex=query_presets.Sex.FEMALE, + disease_state=query_presets.DiseaseState.UNAFFECTED, + ), + query_presets.PedigreeMember( + family="FAM", + name="mother", + father=query_presets.NOBODY, + mother=query_presets.NOBODY, + sex=query_presets.Sex.FEMALE, + disease_state=query_presets.DiseaseState.UNAFFECTED, + ), + ) + # Trio compatible with dominant mode of inheritance where father is also affected + self.trio_dominant = ( + query_presets.PedigreeMember( + family="FAM", + name="index", + father="father", + mother="mother", + sex=query_presets.Sex.MALE, + disease_state=query_presets.DiseaseState.AFFECTED, + ), + query_presets.PedigreeMember( + family="FAM", + name="father", + father=query_presets.NOBODY, + mother=query_presets.NOBODY, + sex=query_presets.Sex.FEMALE, + disease_state=query_presets.DiseaseState.AFFECTED, + ), + query_presets.PedigreeMember( + family="FAM", + name="mother", + father=query_presets.NOBODY, + mother=query_presets.NOBODY, + sex=query_presets.Sex.FEMALE, + disease_state=query_presets.DiseaseState.UNAFFECTED, + ), + ) + + +class TestEnumInheritance(PedigreesMixin, TestCase): + def testValues(self): + self.assertEqual(query_presets.Inheritance.DOMINANT.value, "dominant") + self.assertEqual( + query_presets.Inheritance.HOMOZYGOUS_RECESSIVE.value, "homozygous_recessive" + ) + self.assertEqual( + query_presets.Inheritance.COMPOUND_HETEROZYGOUS.value, "compound_heterozygous" + ) + self.assertEqual(query_presets.Inheritance.RECESSIVE.value, "recessive") + self.assertEqual(query_presets.Inheritance.X_RECESSIVE.value, "x_recessive") + self.assertEqual(query_presets.Inheritance.AFFECTED_CARRIERS.value, "affected_carriers") + self.assertEqual(query_presets.Inheritance.CUSTOM.value, "custom") + self.assertEqual(query_presets.Inheritance.ANY.value, "any") + + def testToSettingsDominant(self): + # singleton + actual = query_presets.Inheritance.DOMINANT.to_settings(self.singleton, self.singleton[0]) + self.assertEqual( + actual, + { + "genotype": {"index": query_presets.GenotypeChoice.HET}, + "recessive_index": None, + "recessive_mode": None, + }, + ) + # child with father + actual = query_presets.Inheritance.DOMINANT.to_settings( + self.child_father, self.child_father[0].name + ) + self.assertEqual( + actual, + { + "recessive_index": None, + "recessive_mode": None, + "genotype": { + "index": query_presets.GenotypeChoice.HET, + "father": query_presets.GenotypeChoice.HET, + }, + }, + ) + # child with mother + actual = query_presets.Inheritance.DOMINANT.to_settings( + self.child_mother, self.child_mother[0].name + ) + self.assertEqual( + actual, + { + "recessive_index": None, + "recessive_mode": None, + "genotype": { + "index": query_presets.GenotypeChoice.HET, + "mother": query_presets.GenotypeChoice.HET, + }, + }, + ) + # trio denovo + actual = query_presets.Inheritance.DOMINANT.to_settings(self.trio_denovo, self.singleton[0]) + self.assertEqual( + actual, + { + "recessive_index": None, + "recessive_mode": None, + "genotype": { + "index": query_presets.GenotypeChoice.HET, + "father": query_presets.GenotypeChoice.REF, + "mother": query_presets.GenotypeChoice.REF, + }, + }, + ) + # trio dominant inherited + actual = query_presets.Inheritance.DOMINANT.to_settings( + self.trio_dominant, self.trio_dominant[0].name + ) + self.assertEqual( + actual, + { + "recessive_index": None, + "recessive_mode": None, + "genotype": { + "index": query_presets.GenotypeChoice.HET, + "father": query_presets.GenotypeChoice.HET, + "mother": query_presets.GenotypeChoice.REF, + }, + }, + ) + + def testToSettingsHomozygousRecessive(self): + # singleton + actual = query_presets.Inheritance.HOMOZYGOUS_RECESSIVE.to_settings( + self.singleton, self.singleton[0].name + ) + self.assertEqual( + actual, + { + "genotype": {"index": query_presets.GenotypeChoice.HOM}, + "recessive_index": "index", + "recessive_mode": None, + }, + ) + # child with father + actual = query_presets.Inheritance.HOMOZYGOUS_RECESSIVE.to_settings( + self.child_father, self.child_father[0].name + ) + self.assertEqual( + actual, + { + "recessive_index": "index", + "recessive_mode": None, + "genotype": { + "index": query_presets.GenotypeChoice.HOM, + "father": query_presets.GenotypeChoice.HOM, + }, + }, + ) + # child with mother + actual = query_presets.Inheritance.HOMOZYGOUS_RECESSIVE.to_settings( + self.child_mother, self.child_mother[0].name + ) + self.assertEqual( + actual, + { + "recessive_index": "index", + "recessive_mode": None, + "genotype": { + "index": query_presets.GenotypeChoice.HOM, + "mother": query_presets.GenotypeChoice.HOM, + }, + }, + ) + # trio denovo + actual = query_presets.Inheritance.HOMOZYGOUS_RECESSIVE.to_settings( + self.trio_denovo, self.trio_denovo[0].name + ) + self.assertEqual( + actual, + { + "recessive_index": "index", + "recessive_mode": None, + "genotype": { + "index": query_presets.GenotypeChoice.HOM, + "father": query_presets.GenotypeChoice.HET, + "mother": query_presets.GenotypeChoice.HET, + }, + }, + ) + # trio dominant inherited + actual = query_presets.Inheritance.HOMOZYGOUS_RECESSIVE.to_settings( + self.trio_dominant, self.trio_dominant[0].name + ) + self.assertEqual( + actual, + { + "recessive_index": "index", + "recessive_mode": None, + "genotype": { + "index": query_presets.GenotypeChoice.HOM, + "father": query_presets.GenotypeChoice.HOM, + "mother": query_presets.GenotypeChoice.REF, + }, + }, + ) + + def testToSettingsCompoundHeterozygous(self): + # singleton + actual = query_presets.Inheritance.COMPOUND_HETEROZYGOUS.to_settings( + self.singleton, self.singleton[0].name + ) + self.assertEqual( + actual, + { + "genotype": {"index": None}, + "recessive_index": "index", + "recessive_mode": "compound-recessive", + }, + ) + # child with father + actual = query_presets.Inheritance.COMPOUND_HETEROZYGOUS.to_settings( + self.child_father, self.child_father[0].name + ) + self.assertEqual( + actual, + { + "recessive_index": "index", + "recessive_mode": "compound-recessive", + "genotype": {"index": None, "father": None,}, + }, + ) + # child with mother + actual = query_presets.Inheritance.COMPOUND_HETEROZYGOUS.to_settings( + self.child_mother, self.child_mother[0].name + ) + self.assertEqual( + actual, + { + "recessive_index": "index", + "recessive_mode": "compound-recessive", + "genotype": {"index": None, "mother": None,}, + }, + ) + # trio denovo + actual = query_presets.Inheritance.COMPOUND_HETEROZYGOUS.to_settings( + self.trio_denovo, self.trio_denovo[0].name + ) + self.assertEqual( + actual, + { + "recessive_index": "index", + "recessive_mode": "compound-recessive", + "genotype": {"index": None, "father": None, "mother": None,}, + }, + ) + # trio dominant inherited + actual = query_presets.Inheritance.COMPOUND_HETEROZYGOUS.to_settings( + self.trio_dominant, self.trio_dominant[0].name + ) + self.assertEqual( + actual, + { + "recessive_index": "index", + "recessive_mode": "compound-recessive", + "genotype": {"index": None, "father": None, "mother": None,}, + }, + ) + + def testToSettingsRecessive(self): + # singleton + actual = query_presets.Inheritance.RECESSIVE.to_settings( + self.singleton, self.singleton[0].name + ) + self.assertEqual( + actual, + { + "genotype": {"index": None}, + "recessive_index": "index", + "recessive_mode": "recessive", + }, + ) + # child with father + actual = query_presets.Inheritance.RECESSIVE.to_settings( + self.child_father, self.child_father[0].name + ) + self.assertEqual( + actual, + { + "recessive_index": "index", + "recessive_mode": "recessive", + "genotype": {"index": None, "father": None,}, + }, + ) + # child with mother + actual = query_presets.Inheritance.RECESSIVE.to_settings( + self.child_mother, self.child_mother[0].name + ) + self.assertEqual( + actual, + { + "recessive_index": "index", + "recessive_mode": "recessive", + "genotype": {"index": None, "mother": None,}, + }, + ) + # trio denovo + actual = query_presets.Inheritance.RECESSIVE.to_settings( + self.trio_denovo, self.trio_denovo[0].name + ) + self.assertEqual( + actual, + { + "recessive_index": "index", + "recessive_mode": "recessive", + "genotype": {"index": None, "father": None, "mother": None,}, + }, + ) + # trio dominant inherited + actual = query_presets.Inheritance.RECESSIVE.to_settings( + self.trio_dominant, self.trio_dominant[0].name + ) + self.assertEqual( + actual, + { + "recessive_index": "index", + "recessive_mode": "recessive", + "genotype": {"index": None, "father": None, "mother": None,}, + }, + ) + + def testToSettingsXRecessive(self): + # singleton + actual = query_presets.Inheritance.X_RECESSIVE.to_settings( + self.singleton, self.singleton[0].name + ) + self.assertEqual( + actual, + { + "genotype": {"index": query_presets.GenotypeChoice.HOM}, + "recessive_index": self.singleton[0].name, + "recessive_mode": None, + }, + ) + # child with father + actual = query_presets.Inheritance.X_RECESSIVE.to_settings( + self.child_father, self.child_father[0].name + ) + self.assertEqual( + actual, + { + "recessive_index": self.child_father[0].name, + "recessive_mode": None, + "genotype": { + "index": query_presets.GenotypeChoice.HOM, + "father": query_presets.GenotypeChoice.HOM, + }, + }, + ) + # child with mother + actual = query_presets.Inheritance.X_RECESSIVE.to_settings( + self.child_mother, self.child_mother[0].name + ) + self.assertEqual( + actual, + { + "recessive_index": self.child_father[0].name, + "recessive_mode": None, + "genotype": { + "index": query_presets.GenotypeChoice.HOM, + "mother": query_presets.GenotypeChoice.ANY, + }, + }, + ) + # trio denovo + actual = query_presets.Inheritance.X_RECESSIVE.to_settings( + self.trio_denovo, self.trio_denovo[0].name + ) + self.assertEqual( + actual, + { + "recessive_index": self.trio_denovo[0].name, + "recessive_mode": None, + "genotype": { + "index": query_presets.GenotypeChoice.HOM, + "father": query_presets.GenotypeChoice.REF, + "mother": query_presets.GenotypeChoice.HET, + }, + }, + ) + # trio dominant inherited + actual = query_presets.Inheritance.X_RECESSIVE.to_settings( + self.trio_dominant, self.trio_dominant[0].name + ) + self.assertEqual( + actual, + { + "recessive_index": self.trio_dominant[0].name, + "recessive_mode": None, + "genotype": { + "index": query_presets.GenotypeChoice.HOM, + "father": query_presets.GenotypeChoice.HOM, + "mother": query_presets.GenotypeChoice.REF, + }, + }, + ) + + def testToSettingsAffectedCarriers(self): + # singleton + actual = query_presets.Inheritance.AFFECTED_CARRIERS.to_settings( + self.singleton, self.singleton[0].name + ) + self.assertEqual( + actual, + { + "genotype": {"index": query_presets.GenotypeChoice.VARIANT}, + "recessive_index": None, + "recessive_mode": None, + }, + ) + # child with father + actual = query_presets.Inheritance.AFFECTED_CARRIERS.to_settings( + self.child_father, self.child_father[0].name + ) + self.assertEqual( + actual, + { + "recessive_index": None, + "recessive_mode": None, + "genotype": { + "index": query_presets.GenotypeChoice.VARIANT, + "father": query_presets.GenotypeChoice.VARIANT, + }, + }, + ) + # child with mother + actual = query_presets.Inheritance.AFFECTED_CARRIERS.to_settings( + self.child_mother, self.child_mother[0].name + ) + self.assertEqual( + actual, + { + "recessive_index": None, + "recessive_mode": None, + "genotype": { + "index": query_presets.GenotypeChoice.VARIANT, + "mother": query_presets.GenotypeChoice.VARIANT, + }, + }, + ) + # trio denovo + actual = query_presets.Inheritance.AFFECTED_CARRIERS.to_settings( + self.trio_denovo, self.trio_denovo[0].name + ) + self.assertEqual( + actual, + { + "recessive_index": None, + "recessive_mode": None, + "genotype": { + "index": query_presets.GenotypeChoice.VARIANT, + "father": query_presets.GenotypeChoice.REF, + "mother": query_presets.GenotypeChoice.REF, + }, + }, + ) + # trio dominant inherited + actual = query_presets.Inheritance.AFFECTED_CARRIERS.to_settings( + self.trio_dominant, self.trio_dominant[0].name + ) + self.assertEqual( + actual, + { + "recessive_index": None, + "recessive_mode": None, + "genotype": { + "index": query_presets.GenotypeChoice.VARIANT, + "father": query_presets.GenotypeChoice.VARIANT, + "mother": query_presets.GenotypeChoice.REF, + }, + }, + ) + + +class TestEnumFrequency(TestCase): + def testValues(self): + self.assertEqual( + query_presets.Frequency.DOMINANT_SUPER_STRICT.value, "dominant_super_strict" + ) + self.assertEqual(query_presets.Frequency.DOMINANT_STRICT.value, "dominant_strict") + self.assertEqual(query_presets.Frequency.DOMINANT_RELAXED.value, "dominant_relaxed") + self.assertEqual(query_presets.Frequency.RECESSIVE_STRICT.value, "recessive_strict") + self.assertEqual(query_presets.Frequency.RECESSIVE_RELAXED.value, "recessive_relaxed") + self.assertEqual(query_presets.Frequency.ANY.value, "any") + + def testToSettingsDominantSuperStrict(self): + self.assertEqual( + query_presets.Frequency.DOMINANT_SUPER_STRICT.to_settings(), + { + "thousand_genomes_enabled": True, + "thousand_genomes_homozygous": 0, + "thousand_genomes_heterozygous": 1, + "thousand_genomes_hemizygous": None, + "thousand_genomes_frequency": 0.002, + "exac_enabled": True, + "exac_homozygous": 0, + "exac_heterozygous": 1, + "exac_hemizygous": None, + "exac_frequency": 0.002, + "gnomad_exomes_enabled": True, + "gnomad_exomes_homozygous": 0, + "gnomad_exomes_heterozygous": 1, + "gnomad_exomes_hemizygous": None, + "gnomad_exomes_frequency": 0.002, + "gnomad_genomes_enabled": True, + "gnomad_genomes_homozygous": 0, + "gnomad_genomes_heterozygous": 1, + "gnomad_genomes_hemizygous": None, + "gnomad_genomes_frequency": 0.002, + "inhouse_enabled": True, + "inhouse_homozygous": None, + "inhouse_heterozygous": None, + "inhouse_hemizygous": None, + "inhouse_carriers": 20, + "mtdb_enabled": False, + "mtdb_count": None, + "mtdb_frequency": None, + "helixmtdb_enabled": False, + "helixmtdb_het_count": None, + "helixmtdb_hom_count": None, + "helixmtdb_frequency": None, + "mitomap_enabled": False, + "mitomap_count": None, + "mitomap_frequency": None, + }, + ) + + def testToSettingsDominantStrict(self): + self.assertEqual( + query_presets.Frequency.DOMINANT_STRICT.to_settings(), + { + "thousand_genomes_enabled": True, + "thousand_genomes_homozygous": 0, + "thousand_genomes_heterozygous": 4, + "thousand_genomes_hemizygous": None, + "thousand_genomes_frequency": 0.002, + "exac_enabled": True, + "exac_homozygous": 0, + "exac_heterozygous": 10, + "exac_hemizygous": None, + "exac_frequency": 0.002, + "gnomad_exomes_enabled": True, + "gnomad_exomes_homozygous": 0, + "gnomad_exomes_heterozygous": 20, + "gnomad_exomes_hemizygous": None, + "gnomad_exomes_frequency": 0.002, + "gnomad_genomes_enabled": True, + "gnomad_genomes_homozygous": 0, + "gnomad_genomes_heterozygous": 4, + "gnomad_genomes_hemizygous": None, + "gnomad_genomes_frequency": 0.002, + "inhouse_enabled": True, + "inhouse_homozygous": None, + "inhouse_heterozygous": None, + "inhouse_hemizygous": None, + "inhouse_carriers": 20, + "mtdb_enabled": True, + "mtdb_count": 10, + "mtdb_frequency": 0.01, + "helixmtdb_enabled": True, + "helixmtdb_hom_count": 200, + "helixmtdb_het_count": None, + "helixmtdb_frequency": 0.01, + "mitomap_enabled": False, + "mitomap_count": None, + "mitomap_frequency": None, + }, + ) + + def testToSettingsDominantRelaxed(self): + self.assertEqual( + query_presets.Frequency.DOMINANT_RELAXED.to_settings(), + { + "thousand_genomes_enabled": True, + "thousand_genomes_homozygous": 0, + "thousand_genomes_heterozygous": 10, + "thousand_genomes_hemizygous": None, + "thousand_genomes_frequency": 0.01, + "exac_enabled": True, + "exac_homozygous": 0, + "exac_heterozygous": 25, + "exac_hemizygous": None, + "exac_frequency": 0.01, + "gnomad_exomes_enabled": True, + "gnomad_exomes_homozygous": 0, + "gnomad_exomes_heterozygous": 50, + "gnomad_exomes_hemizygous": None, + "gnomad_exomes_frequency": 0.01, + "gnomad_genomes_enabled": True, + "gnomad_genomes_homozygous": 0, + "gnomad_genomes_heterozygous": 20, + "gnomad_genomes_hemizygous": None, + "gnomad_genomes_frequency": 0.01, + "inhouse_enabled": True, + "inhouse_homozygous": None, + "inhouse_heterozygous": None, + "inhouse_hemizygous": None, + "inhouse_carriers": 20, + "mtdb_enabled": True, + "mtdb_count": 50, + "mtdb_frequency": 0.15, + "helixmtdb_enabled": True, + "helixmtdb_het_count": None, + "helixmtdb_hom_count": 400, + "helixmtdb_frequency": 0.15, + "mitomap_enabled": False, + "mitomap_count": None, + "mitomap_frequency": None, + }, + ) + + def testToSettingsRecessiveStrict(self): + self.assertEqual( + query_presets.Frequency.RECESSIVE_STRICT.to_settings(), + { + "thousand_genomes_enabled": True, + "thousand_genomes_homozygous": 0, + "thousand_genomes_heterozygous": 24, + "thousand_genomes_hemizygous": None, + "thousand_genomes_frequency": 0.001, + "exac_enabled": True, + "exac_homozygous": 0, + "exac_heterozygous": 60, + "exac_hemizygous": None, + "exac_frequency": 0.001, + "gnomad_exomes_enabled": True, + "gnomad_exomes_homozygous": 0, + "gnomad_exomes_heterozygous": 120, + "gnomad_exomes_hemizygous": None, + "gnomad_exomes_frequency": 0.001, + "gnomad_genomes_enabled": True, + "gnomad_genomes_homozygous": 0, + "gnomad_genomes_heterozygous": 15, + "gnomad_genomes_hemizygous": None, + "gnomad_genomes_frequency": 0.001, + "inhouse_enabled": True, + "inhouse_homozygous": None, + "inhouse_heterozygous": None, + "inhouse_hemizygous": None, + "inhouse_carriers": 20, + "mtdb_enabled": False, + "mtdb_count": None, + "mtdb_frequency": None, + "helixmtdb_enabled": False, + "helixmtdb_het_count": None, + "helixmtdb_hom_count": None, + "helixmtdb_frequency": None, + "mitomap_enabled": False, + "mitomap_count": None, + "mitomap_frequency": None, + }, + ) + + def testToSettingsRecessiveRelaxed(self): + self.assertEqual( + query_presets.Frequency.RECESSIVE_RELAXED.to_settings(), + { + "thousand_genomes_enabled": True, + "thousand_genomes_homozygous": 4, + "thousand_genomes_heterozygous": 240, + "thousand_genomes_hemizygous": None, + "thousand_genomes_frequency": 0.01, + "exac_enabled": True, + "exac_homozygous": 10, + "exac_heterozygous": 600, + "exac_hemizygous": None, + "exac_frequency": 0.01, + "gnomad_exomes_enabled": True, + "gnomad_exomes_homozygous": 20, + "gnomad_exomes_heterozygous": 1200, + "gnomad_exomes_hemizygous": None, + "gnomad_exomes_frequency": 0.01, + "gnomad_genomes_enabled": True, + "gnomad_genomes_homozygous": 4, + "gnomad_genomes_heterozygous": 150, + "gnomad_genomes_hemizygous": None, + "gnomad_genomes_frequency": 0.01, + "inhouse_enabled": True, + "inhouse_homozygous": None, + "inhouse_heterozygous": None, + "inhouse_hemizygous": None, + "inhouse_carriers": 20, + "mtdb_enabled": False, + "mtdb_count": None, + "mtdb_frequency": None, + "helixmtdb_enabled": False, + "helixmtdb_het_count": None, + "helixmtdb_hom_count": None, + "helixmtdb_frequency": None, + "mitomap_enabled": False, + "mitomap_count": None, + "mitomap_frequency": None, + }, + ) + + def testToSettingsAny(self): + self.assertEqual( + query_presets.Frequency.ANY.to_settings(), + { + "thousand_genomes_enabled": False, + "thousand_genomes_homozygous": None, + "thousand_genomes_heterozygous": None, + "thousand_genomes_hemizygous": None, + "thousand_genomes_frequency": None, + "exac_enabled": False, + "exac_homozygous": None, + "exac_heterozygous": None, + "exac_hemizygous": None, + "exac_frequency": None, + "gnomad_exomes_enabled": False, + "gnomad_exomes_homozygous": None, + "gnomad_exomes_heterozygous": None, + "gnomad_exomes_hemizygous": None, + "gnomad_exomes_frequency": None, + "gnomad_genomes_enabled": False, + "gnomad_genomes_homozygous": None, + "gnomad_genomes_heterozygous": None, + "gnomad_genomes_hemizygous": None, + "gnomad_genomes_frequency": None, + "inhouse_enabled": False, + "inhouse_homozygous": None, + "inhouse_heterozygous": None, + "inhouse_carriers": None, + "mtdb_enabled": False, + "mtdb_count": None, + "mtdb_frequency": None, + "helixmtdb_enabled": False, + "helixmtdb_het_count": None, + "helixmtdb_hom_count": None, + "helixmtdb_frequency": None, + "mitomap_enabled": False, + "mitomap_count": None, + "mitomap_frequency": None, + }, + ) + + +class TestEnumImpact(TestCase): + def testValues(self): + self.assertEqual(query_presets.Impact.NULL_VARIANT.value, "null_variant") + self.assertEqual(query_presets.Impact.AA_CHANGE_SPLICING.value, "aa_change_splicing") + self.assertEqual( + query_presets.Impact.ALL_CODING_DEEP_INTRONIC.value, "all_coding_deep_intronic" + ) + self.assertEqual(query_presets.Impact.WHOLE_TRANSCRIPT.value, "whole_transcript") + self.assertEqual(query_presets.Impact.ANY.value, "any") + + def testToSettingsNullVariant(self): + self.assertEqual( + query_presets.Impact.NULL_VARIANT.to_settings(), + { + "var_type_snv": True, + "var_type_mnv": True, + "var_type_indel": True, + "transcripts_coding": True, + "transcripts_noncoding": False, + "effects": ( + "exon_loss_variant", + "feature_truncation", + "frameshift_elongation", + "frameshift_truncation", + "frameshift_variant", + "internal_feature_elongation", + "splice_acceptor_variant", + "splice_donor_variant", + "start_lost", + "stop_gained", + "stop_lost", + "structural_variant", + "transcript_ablation", + ), + }, + ) + + def testToSettingsAaChangeSplicing(self): + self.assertEqual( + query_presets.Impact.AA_CHANGE_SPLICING.to_settings(), + { + "var_type_snv": True, + "var_type_mnv": True, + "var_type_indel": True, + "transcripts_coding": True, + "transcripts_noncoding": False, + "effects": ( + "complex_substitution", + "direct_tandem_duplication", + "disruptive_inframe_deletion", + "disruptive_inframe_insertion", + "exon_loss_variant", + "feature_truncation", + "frameshift_elongation", + "frameshift_truncation", + "frameshift_variant", + "inframe_deletion", + "inframe_insertion", + "internal_feature_elongation", + "missense_variant", + "mnv", + "splice_acceptor_variant", + "splice_donor_variant", + "splice_region_variant", + "start_lost", + "stop_gained", + "stop_lost", + "structural_variant", + "transcript_ablation", + ), + }, + ) + + def testToSettingsAllCodingDeepIntronic(self): + self.assertEqual( + query_presets.Impact.ALL_CODING_DEEP_INTRONIC.to_settings(), + { + "var_type_snv": True, + "var_type_mnv": True, + "var_type_indel": True, + "transcripts_coding": True, + "transcripts_noncoding": False, + "effects": ( + "coding_transcript_intron_variant", + "complex_substitution", + "direct_tandem_duplication", + "disruptive_inframe_deletion", + "disruptive_inframe_insertion", + "exon_loss_variant", + "feature_truncation", + "frameshift_elongation", + "frameshift_truncation", + "frameshift_variant", + "inframe_deletion", + "inframe_insertion", + "internal_feature_elongation", + "missense_variant", + "mnv", + "splice_acceptor_variant", + "splice_donor_variant", + "splice_region_variant", + "start_lost", + "stop_gained", + "stop_lost", + "structural_variant", + "synonymous_variant", + "transcript_ablation", + ), + }, + ) + + def testToSettingsWholeTranscript(self): + self.assertEqual( + query_presets.Impact.WHOLE_TRANSCRIPT.to_settings(), + { + "var_type_snv": True, + "var_type_mnv": True, + "var_type_indel": True, + "transcripts_coding": True, + "transcripts_noncoding": False, + "effects": ( + "coding_transcript_intron_variant", + "complex_substitution", + "direct_tandem_duplication", + "disruptive_inframe_deletion", + "disruptive_inframe_insertion", + "exon_loss_variant", + "feature_truncation", + "five_prime_UTR_exon_variant", + "five_prime_UTR_intron_variant", + "frameshift_elongation", + "frameshift_truncation", + "frameshift_variant", + "inframe_deletion", + "inframe_insertion", + "internal_feature_elongation", + "missense_variant", + "mnv", + "splice_acceptor_variant", + "splice_donor_variant", + "splice_region_variant", + "start_lost", + "stop_gained", + "stop_lost", + "stop_retained_variant", + "structural_variant", + "synonymous_variant", + "three_prime_UTR_exon_variant", + "three_prime_UTR_intron_variant", + "transcript_ablation", + ), + }, + ) + + def testToSettingsAny(self): + self.assertEqual( + query_presets.Impact.ANY.to_settings(), + { + "var_type_snv": True, + "var_type_mnv": True, + "var_type_indel": True, + "transcripts_coding": True, + "transcripts_noncoding": True, + "effects": ( + "coding_transcript_intron_variant", + "complex_substitution", + "direct_tandem_duplication", + "disruptive_inframe_deletion", + "disruptive_inframe_insertion", + "downstream_gene_variant", + "exon_loss_variant", + "feature_truncation", + "five_prime_UTR_exon_variant", + "five_prime_UTR_intron_variant", + "frameshift_elongation", + "frameshift_truncation", + "frameshift_variant", + "inframe_deletion", + "inframe_insertion", + "intergenic_variant", + "internal_feature_elongation", + "missense_variant", + "mnv", + "non_coding_transcript_exon_variant", + "non_coding_transcript_intron_variant", + "splice_acceptor_variant", + "splice_donor_variant", + "splice_region_variant", + "start_lost", + "stop_gained", + "stop_lost", + "stop_retained_variant", + "structural_variant", + "synonymous_variant", + "three_prime_UTR_exon_variant", + "three_prime_UTR_intron_variant", + "transcript_ablation", + "upstream_gene_variant", + ), + }, + ) + + +class TestEnumQualityFail(TestCase): + def testValues(self): + self.assertEqual(query_presets._QualityFail.DROP_VARIANT.value, "drop-variant") + self.assertEqual(query_presets._QualityFail.IGNORE.value, "ignore") + self.assertEqual(query_presets._QualityFail.NO_CALL.value, "no-call") + + +class TestEnumQuality(PedigreesMixin, TestCase): + def testValues(self): + self.assertEqual(query_presets.Quality.SUPER_STRICT.value, "super_strict") + self.assertEqual(query_presets.Quality.STRICT.value, "strict") + self.assertEqual(query_presets.Quality.RELAXED.value, "relaxed") + self.assertEqual(query_presets.Quality.ANY.value, "any") + + def testToSettingsSuperStrict(self): + self.assertEqual( + query_presets.Quality.SUPER_STRICT.to_settings(self.trio_denovo), + { + "father": { + "ab": 0.3, + "ad": 3, + "ad_max": None, + "dp_het": 10, + "dp_hom": 5, + "fail": "drop-variant", + "gq": 30, + }, + "index": { + "ab": 0.3, + "ad": 3, + "ad_max": None, + "dp_het": 10, + "dp_hom": 5, + "fail": "drop-variant", + "gq": 30, + }, + "mother": { + "ab": 0.3, + "ad": 3, + "ad_max": None, + "dp_het": 10, + "dp_hom": 5, + "fail": "drop-variant", + "gq": 30, + }, + }, + ) + + def testToSettingsStrict(self): + self.assertEqual( + query_presets.Quality.STRICT.to_settings(self.trio_denovo), + { + "father": { + "ab": 0.2, + "ad": 3, + "ad_max": None, + "dp_het": 10, + "dp_hom": 5, + "fail": "drop-variant", + "gq": 10, + }, + "index": { + "ab": 0.2, + "ad": 3, + "ad_max": None, + "dp_het": 10, + "dp_hom": 5, + "fail": "drop-variant", + "gq": 10, + }, + "mother": { + "ab": 0.2, + "ad": 3, + "ad_max": None, + "dp_het": 10, + "dp_hom": 5, + "fail": "drop-variant", + "gq": 10, + }, + }, + ) + + def testToSettingsRelaxed(self): + self.assertEqual( + query_presets.Quality.RELAXED.to_settings(self.trio_denovo), + { + "father": { + "ab": 0.1, + "ad": 2, + "ad_max": None, + "dp_het": 8, + "dp_hom": 4, + "fail": "drop-variant", + "gq": 10, + }, + "index": { + "ab": 0.1, + "ad": 2, + "ad_max": None, + "dp_het": 8, + "dp_hom": 4, + "fail": "drop-variant", + "gq": 10, + }, + "mother": { + "ab": 0.1, + "ad": 2, + "ad_max": None, + "dp_het": 8, + "dp_hom": 4, + "fail": "drop-variant", + "gq": 10, + }, + }, + ) + + def testToSettingsAny(self): + self.assertEqual( + query_presets.Quality.ANY.to_settings(self.trio_denovo), + { + "father": { + "ab": 0.0, + "ad": 0, + "ad_max": None, + "dp_het": 0, + "dp_hom": 0, + "fail": "ignore", + "gq": 0, + }, + "index": { + "ab": 0.0, + "ad": 0, + "ad_max": None, + "dp_het": 0, + "dp_hom": 0, + "fail": "ignore", + "gq": 0, + }, + "mother": { + "ab": 0.0, + "ad": 0, + "ad_max": None, + "dp_het": 0, + "dp_hom": 0, + "fail": "ignore", + "gq": 0, + }, + }, + ) + + +class TestEnumChromosomes(TestCase): + def testValues(self): + self.assertEqual(query_presets.Chromosomes.WHOLE_GENOME.value, "whole_genome") + self.assertEqual(query_presets.Chromosomes.AUTOSOMES.value, "autosomes") + self.assertEqual(query_presets.Chromosomes.X_CHROMOSOME.value, "x_chromosome") + self.assertEqual(query_presets.Chromosomes.Y_CHROMOSOME.value, "y_chromosome") + self.assertEqual(query_presets.Chromosomes.MT_CHROMOSOME.value, "mt_chromosome") + + def testToSettingsWholeGenome(self): + self.assertEqual( + query_presets.Chromosomes.WHOLE_GENOME.to_settings(), + {"genomic_region": (), "gene_allowlist": (), "gene_blocklist": (),}, + ) + + def testToSettingsAutosomes(self): + self.assertEqual( + query_presets.Chromosomes.AUTOSOMES.to_settings(), + { + "genomic_region": tuple(f"chr{num}" for num in range(1, 23)), + "gene_allowlist": (), + "gene_blocklist": (), + }, + ) + + def testToSettingsXChromosome(self): + self.assertEqual( + query_presets.Chromosomes.X_CHROMOSOME.to_settings(), + {"genomic_region": ("chrX",), "gene_allowlist": (), "gene_blocklist": (),}, + ) + + def testToSettingsYChromosome(self): + self.assertEqual( + query_presets.Chromosomes.Y_CHROMOSOME.to_settings(), + {"genomic_region": ("chrY",), "gene_allowlist": (), "gene_blocklist": (),}, + ) + + def testToSettingsMTChromosome(self): + self.assertEqual( + query_presets.Chromosomes.MT_CHROMOSOME.to_settings(), + {"genomic_region": ("chrMT",), "gene_allowlist": (), "gene_blocklist": (),}, + ) + + +class TestEnumFlagsEtc(TestCase): + def testValues(self): + self.assertEqual(query_presets.FlagsEtc.DEFAULTS.value, "defaults") + self.assertEqual(query_presets.FlagsEtc.CLINVAR_ONLY.value, "clinvar_only") + self.assertEqual(query_presets.FlagsEtc.USER_FLAGGED.value, "user_flagged") + + def testToSettingsDefaults(self): + self.assertEqual( + query_presets.FlagsEtc.DEFAULTS.to_settings(), + { + "clinvar_include_benign": False, + "clinvar_include_likely_benign": False, + "clinvar_include_likely_pathogenic": True, + "clinvar_include_pathogenic": True, + "clinvar_include_uncertain_significance": False, + "flag_bookmarked": True, + "flag_candidate": True, + "flag_doesnt_segregate": True, + "flag_final_causative": True, + "flag_for_validation": True, + "flag_no_disease_association": True, + "flag_phenotype_match_empty": True, + "flag_phenotype_match_negative": True, + "flag_phenotype_match_positive": True, + "flag_phenotype_match_uncertain": True, + "flag_segregates": True, + "flag_simple_empty": True, + "flag_summary_empty": True, + "flag_summary_negative": True, + "flag_summary_positive": True, + "flag_summary_uncertain": True, + "flag_validation_empty": True, + "flag_validation_negative": True, + "flag_validation_positive": True, + "flag_validation_uncertain": True, + "flag_visual_empty": True, + "flag_visual_negative": True, + "flag_visual_positive": True, + "flag_visual_uncertain": True, + "remove_if_in_dbsnp": False, + "require_in_clinvar": False, + "require_in_hgmd_public": False, + }, + ) + + def testToSettingsClinvarOnly(self): + self.assertEqual( + query_presets.FlagsEtc.CLINVAR_ONLY.to_settings(), + { + "flag_bookmarked": True, + "flag_candidate": True, + "flag_doesnt_segregate": True, + "flag_final_causative": True, + "flag_for_validation": True, + "flag_no_disease_association": True, + "flag_phenotype_match_empty": True, + "flag_phenotype_match_negative": True, + "flag_phenotype_match_positive": True, + "flag_phenotype_match_uncertain": True, + "flag_segregates": True, + "flag_simple_empty": True, + "flag_summary_empty": True, + "flag_summary_negative": True, + "flag_summary_positive": True, + "flag_summary_uncertain": True, + "flag_validation_empty": True, + "flag_validation_negative": True, + "flag_validation_positive": True, + "flag_validation_uncertain": True, + "flag_visual_empty": True, + "flag_visual_negative": True, + "flag_visual_positive": True, + "flag_visual_uncertain": True, + "remove_if_in_dbsnp": False, + "require_in_clinvar": True, + "require_in_hgmd_public": False, + }, + ) + + def testToSettingsUserFlagged(self): + self.assertEqual( + query_presets.FlagsEtc.USER_FLAGGED.to_settings(), + { + "clinvar_include_benign": False, + "clinvar_include_likely_benign": False, + "clinvar_include_likely_pathogenic": True, + "clinvar_include_pathogenic": True, + "clinvar_include_uncertain_significance": False, + "flag_bookmarked": True, + "flag_candidate": True, + "flag_doesnt_segregate": True, + "flag_final_causative": True, + "flag_for_validation": True, + "flag_no_disease_association": True, + "flag_phenotype_match_empty": False, + "flag_phenotype_match_negative": True, + "flag_phenotype_match_positive": True, + "flag_phenotype_match_uncertain": True, + "flag_segregates": True, + "flag_simple_empty": False, + "flag_summary_empty": False, + "flag_summary_negative": True, + "flag_summary_positive": True, + "flag_summary_uncertain": True, + "flag_validation_empty": False, + "flag_validation_negative": True, + "flag_validation_positive": True, + "flag_validation_uncertain": True, + "flag_visual_empty": False, + "flag_visual_negative": True, + "flag_visual_positive": True, + "flag_visual_uncertain": True, + "remove_if_in_dbsnp": False, + "require_in_clinvar": False, + "require_in_hgmd_public": False, + }, + ) + + +class TestQuickPresets(PedigreesMixin, TestCase): + def testValueDefaults(self): + self.assertEqual( + str(query_presets.QUICK_PRESETS.defaults), + "QuickPresets(inheritance=, " + "frequency=, " + "impact=, " + "quality=, chromosomes=, " + "flags_etc=)", + ) + + def testValueDeNovo(self): + self.assertEqual( + str(query_presets.QUICK_PRESETS.de_novo), + "QuickPresets(inheritance=, " + "frequency=, " + "impact=, " + "quality=, chromosomes=, " + "flags_etc=)", + ) + + def testValueDominant(self): + self.assertEqual( + str(query_presets.QUICK_PRESETS.dominant), + "QuickPresets(inheritance=, " + "frequency=, " + "impact=, " + "quality=, chromosomes=, " + "flags_etc=)", + ) + + def testValueHomozygousRecessive(self): + self.assertEqual( + str(query_presets.QUICK_PRESETS.homozygous_recessive), + "QuickPresets(inheritance=, " + "frequency=, " + "impact=, " + "quality=, chromosomes=, " + "flags_etc=)", + ) + + def testValueCompoundRecessive(self): + self.assertEqual( + str(query_presets.QUICK_PRESETS.compound_recessive), + "QuickPresets(inheritance=, " + "frequency=, " + "impact=, " + "quality=, chromosomes=, " + "flags_etc=)", + ) + + def testValueRecessive(self): + self.assertEqual( + str(query_presets.QUICK_PRESETS.recessive), + "QuickPresets(inheritance=, " + "frequency=, " + "impact=, " + "quality=, chromosomes=, " + "flags_etc=)", + ) + + def testValueXRecessive(self): + self.assertEqual( + str(query_presets.QUICK_PRESETS.x_recessive), + "QuickPresets(inheritance=, " + "frequency=, " + "impact=, " + "quality=, chromosomes=, " + "flags_etc=)", + ) + + def testValueClinvarPathogenic(self): + self.assertEqual( + str(query_presets.QUICK_PRESETS.clinvar_pathogenic), + "QuickPresets(inheritance=, " + "frequency=, impact=, " + "quality=, chromosomes=, " + "flags_etc=)", + ) + + def testValueMitochondrial(self): + self.assertEqual( + str(query_presets.QUICK_PRESETS.mitochondrial), + "QuickPresets(inheritance=, " + "frequency=, impact=, " + "quality=, chromosomes=, " + "flags_etc=)", + ) + + def testValueWholeExome(self): + self.assertEqual( + str(query_presets.QUICK_PRESETS.whole_exome), + "QuickPresets(inheritance=, " + "frequency=, " + "impact=, " + "quality=, chromosomes=, " + "flags_etc=)", + ) + + def testToSettingsDefaults(self): + # NB: we only test the full output for the defaults and otherwise just smoke test to_settings. + self.assertEqual( + query_presets.QUICK_PRESETS.defaults.to_settings(self.trio_denovo), + { + "clinvar_include_benign": False, + "clinvar_include_likely_benign": False, + "clinvar_include_likely_pathogenic": True, + "clinvar_include_pathogenic": True, + "clinvar_include_uncertain_significance": False, + "effects": ( + "complex_substitution", + "direct_tandem_duplication", + "disruptive_inframe_deletion", + "disruptive_inframe_insertion", + "exon_loss_variant", + "feature_truncation", + "frameshift_elongation", + "frameshift_truncation", + "frameshift_variant", + "inframe_deletion", + "inframe_insertion", + "internal_feature_elongation", + "missense_variant", + "mnv", + "splice_acceptor_variant", + "splice_donor_variant", + "splice_region_variant", + "start_lost", + "stop_gained", + "stop_lost", + "structural_variant", + "transcript_ablation", + ), + "exac_enabled": True, + "exac_frequency": 0.002, + "exac_hemizygous": None, + "exac_heterozygous": 10, + "exac_homozygous": 0, + "father": { + "ab": 0.2, + "ad": 3, + "ad_max": None, + "dp_het": 10, + "dp_hom": 5, + "fail": "drop-variant", + "gq": 10, + }, + "flag_bookmarked": True, + "flag_candidate": True, + "flag_doesnt_segregate": True, + "flag_final_causative": True, + "flag_for_validation": True, + "flag_no_disease_association": True, + "flag_phenotype_match_empty": True, + "flag_phenotype_match_negative": True, + "flag_phenotype_match_positive": True, + "flag_phenotype_match_uncertain": True, + "flag_segregates": True, + "flag_simple_empty": True, + "flag_summary_empty": True, + "flag_summary_negative": True, + "flag_summary_positive": True, + "flag_summary_uncertain": True, + "flag_validation_empty": True, + "flag_validation_negative": True, + "flag_validation_positive": True, + "flag_validation_uncertain": True, + "flag_visual_empty": True, + "flag_visual_negative": True, + "flag_visual_positive": True, + "flag_visual_uncertain": True, + "gene_allowlist": (), + "gene_blocklist": (), + "genomic_region": (), + "genotype": { + "father": query_presets.GenotypeChoice.ANY, + "index": query_presets.GenotypeChoice.ANY, + "mother": query_presets.GenotypeChoice.ANY, + }, + "gnomad_exomes_enabled": True, + "gnomad_exomes_frequency": 0.002, + "gnomad_exomes_hemizygous": None, + "gnomad_exomes_heterozygous": 20, + "gnomad_exomes_homozygous": 0, + "gnomad_genomes_enabled": True, + "gnomad_genomes_frequency": 0.002, + "gnomad_genomes_hemizygous": None, + "gnomad_genomes_heterozygous": 4, + "gnomad_genomes_homozygous": 0, + "helixmtdb_enabled": True, + "helixmtdb_frequency": 0.01, + "helixmtdb_het_count": None, + "helixmtdb_hom_count": 200, + "index": { + "ab": 0.2, + "ad": 3, + "ad_max": None, + "dp_het": 10, + "dp_hom": 5, + "fail": "drop-variant", + "gq": 10, + }, + "inhouse_carriers": 20, + "inhouse_enabled": True, + "inhouse_hemizygous": None, + "inhouse_heterozygous": None, + "inhouse_homozygous": None, + "mitomap_count": None, + "mitomap_enabled": False, + "mitomap_frequency": None, + "mother": { + "ab": 0.2, + "ad": 3, + "ad_max": None, + "dp_het": 10, + "dp_hom": 5, + "fail": "drop-variant", + "gq": 10, + }, + "mtdb_count": 10, + "mtdb_enabled": True, + "mtdb_frequency": 0.01, + "recessive_index": None, + "recessive_mode": None, + "remove_if_in_dbsnp": False, + "require_in_clinvar": False, + "require_in_hgmd_public": False, + "thousand_genomes_enabled": True, + "thousand_genomes_frequency": 0.002, + "thousand_genomes_hemizygous": None, + "thousand_genomes_heterozygous": 4, + "thousand_genomes_homozygous": 0, + "transcripts_coding": True, + "transcripts_noncoding": False, + "var_type_indel": True, + "var_type_mnv": True, + "var_type_snv": True, + }, + ) + + def testToSettingsOther(self): + self.assertTrue(query_presets.QUICK_PRESETS.defaults.to_settings(self.trio_denovo)) + self.assertTrue(query_presets.QUICK_PRESETS.de_novo.to_settings(self.trio_denovo)) + self.assertTrue(query_presets.QUICK_PRESETS.dominant.to_settings(self.trio_denovo)) + self.assertTrue( + query_presets.QUICK_PRESETS.homozygous_recessive.to_settings(self.trio_denovo) + ) + self.assertTrue( + query_presets.QUICK_PRESETS.compound_recessive.to_settings(self.trio_denovo) + ) + self.assertTrue(query_presets.QUICK_PRESETS.recessive.to_settings(self.trio_denovo)) + self.assertTrue(query_presets.QUICK_PRESETS.x_recessive.to_settings(self.trio_denovo)) + self.assertTrue( + query_presets.QUICK_PRESETS.clinvar_pathogenic.to_settings(self.trio_denovo) + ) + self.assertTrue(query_presets.QUICK_PRESETS.mitochondrial.to_settings(self.trio_denovo)) diff --git a/variants/tests/test_views_api.py b/variants/tests/test_views_api.py index 7809d9225..7a3f93c50 100644 --- a/variants/tests/test_views_api.py +++ b/variants/tests/test_views_api.py @@ -609,3 +609,179 @@ def test_get_access_forbidden(self): expected = 401 response = self.request_knox(url, token=token) self.assertEqual(response.status_code, expected, f"user = {user}") + + +class TestSmallVariantQuerySettingsShortcutApiView( + GenerateSmallVariantResultMixin, ApiViewTestBase +): + """Tests for case query preset generation""" + + def test_get_success(self): + url = reverse( + "variants:api-query-settings-shortcut", kwargs={"case": self.case.sodar_uuid}, + ) + response = self.request_knox(url) + + self.assertEqual(response.status_code, 200) + actual = response.data + expected = { + "presets": { + "inheritance": "any", + "frequency": "dominant_strict", + "impact": "aa_change_splicing", + "quality": "strict", + "chromosomes": "whole_genome", + "flags_etc": "defaults", + }, + "query_settings": { + "recessive_index": None, + "recessive_mode": None, + "genotype": {f"{self.case.index}": "any"}, + "thousand_genomes_enabled": True, + "thousand_genomes_homozygous": 0, + "thousand_genomes_heterozygous": 4, + "thousand_genomes_hemizygous": None, + "thousand_genomes_frequency": 0.002, + "exac_enabled": True, + "exac_homozygous": 0, + "exac_heterozygous": 10, + "exac_hemizygous": None, + "exac_frequency": 0.002, + "gnomad_exomes_enabled": True, + "gnomad_exomes_homozygous": 0, + "gnomad_exomes_heterozygous": 20, + "gnomad_exomes_hemizygous": None, + "gnomad_exomes_frequency": 0.002, + "gnomad_genomes_enabled": True, + "gnomad_genomes_homozygous": 0, + "gnomad_genomes_heterozygous": 4, + "gnomad_genomes_hemizygous": None, + "gnomad_genomes_frequency": 0.002, + "inhouse_enabled": True, + "inhouse_homozygous": None, + "inhouse_heterozygous": None, + "inhouse_hemizygous": None, + "inhouse_carriers": 20, + "mtdb_enabled": True, + "mtdb_count": 10, + "mtdb_frequency": 0.01, + "helixmtdb_enabled": True, + "helixmtdb_hom_count": 200, + "helixmtdb_het_count": None, + "helixmtdb_frequency": 0.01, + "mitomap_enabled": False, + "mitomap_count": None, + "mitomap_frequency": None, + "var_type_snv": True, + "var_type_mnv": True, + "var_type_indel": True, + "transcripts_coding": True, + "transcripts_noncoding": False, + "effects": [ + "complex_substitution", + "direct_tandem_duplication", + "disruptive_inframe_deletion", + "disruptive_inframe_insertion", + "exon_loss_variant", + "feature_truncation", + "frameshift_elongation", + "frameshift_truncation", + "frameshift_variant", + "inframe_deletion", + "inframe_insertion", + "internal_feature_elongation", + "missense_variant", + "mnv", + "splice_acceptor_variant", + "splice_donor_variant", + "splice_region_variant", + "start_lost", + "stop_gained", + "stop_lost", + "structural_variant", + "transcript_ablation", + ], + f"{self.case.index}": { + "dp_het": 10, + "dp_hom": 5, + "ab": 0.2, + "gq": 10, + "ad": 3, + "ad_max": None, + "fail": "drop-variant", + }, + "genomic_region": [], + "gene_allowlist": [], + "gene_blocklist": [], + "clinvar_include_benign": False, + "clinvar_include_likely_benign": False, + "clinvar_include_likely_pathogenic": True, + "clinvar_include_pathogenic": True, + "clinvar_include_uncertain_significance": False, + "flag_bookmarked": True, + "flag_candidate": True, + "flag_doesnt_segregate": True, + "flag_final_causative": True, + "flag_for_validation": True, + "flag_no_disease_association": True, + "flag_phenotype_match_empty": True, + "flag_phenotype_match_negative": True, + "flag_phenotype_match_positive": True, + "flag_phenotype_match_uncertain": True, + "flag_segregates": True, + "flag_simple_empty": True, + "flag_summary_empty": True, + "flag_summary_negative": True, + "flag_summary_positive": True, + "flag_summary_uncertain": True, + "flag_validation_empty": True, + "flag_validation_negative": True, + "flag_validation_positive": True, + "flag_validation_uncertain": True, + "flag_visual_empty": True, + "flag_visual_negative": True, + "flag_visual_positive": True, + "flag_visual_uncertain": True, + "remove_if_in_dbsnp": False, + "require_in_clinvar": False, + "require_in_hgmd_public": False, + }, + } + self.assertEqual(actual, expected) + + def test_get_access_allowed(self): + good_users = [ + self.superuser, + ] + + url = reverse( + "variants:api-query-settings-shortcut", kwargs={"case": self.case.sodar_uuid}, + ) + + for user in good_users: + response = self.request_knox(url, token=self.get_token(user)) + + self.assertEqual(response.status_code, 200, f"user = {user}") + + def test_get_access_forbidden(self): + bad_users = [ + self.guest_as.user, + self.owner_as.user, + self.delegate_as.user, + self.contributor_as.user, + None, + ] + + url = reverse( + "variants:api-query-settings-shortcut", kwargs={"case": self.case.sodar_uuid}, + ) + + for user in bad_users: + if user: + token = self.get_token(user) + expected = 403 + else: + token = EMPTY_KNOX_TOKEN + expected = 401 + response = self.request_knox(url, token=token) + self.assertEqual(response.status_code, expected, f"user = {user}") diff --git a/variants/urls.py b/variants/urls.py index deb5dc87d..78d76ec00 100644 --- a/variants/urls.py +++ b/variants/urls.py @@ -434,6 +434,11 @@ view=views_api.SmallVariantQueryFetchResultsApiView.as_view(), name="api-query-case-fetch-results", ), + url( + regex=r"^api/query-case/query-settings-shortcut/(?P[0-9a-f-]+)/$", + view=views_api.SmallVariantQuerySettingsShortcutApiView.as_view(), + name="api-query-settings-shortcut", + ), ] urlpatterns = ui_urlpatterns + api_urlpatterns diff --git a/variants/views_api.py b/variants/views_api.py index 938c47d3c..f46fc76ab 100644 --- a/variants/views_api.py +++ b/variants/views_api.py @@ -1,14 +1,13 @@ -"""API views for ``variants`` app. - -Currently, the REST API only works for the ``Case`` model. -""" +"""API views for ``variants`` app.""" import typing -import attr +import attrs +import cattr from bgjobs.models import JOB_STATE_FAILED, JOB_STATE_DONE, JOB_STATE_RUNNING, JOB_STATE_INITIAL from django.db.models import Q from projectroles.views_api import SODARAPIGenericProjectMixin, SODARAPIProjectPermission from rest_framework import serializers +from rest_framework.exceptions import NotFound from rest_framework.generics import ( ListAPIView, RetrieveAPIView, @@ -21,12 +20,15 @@ from varfish.api_utils import VarfishApiRenderer, VarfishApiVersioning # # TOOD: timeline update -from .models import Case, SmallVariantQuery, FilterBgJob, SmallVariant -from .serializers import ( +from variants import query_presets +from variants.models import Case, SmallVariantQuery, FilterBgJob, SmallVariant +from variants.serializers import ( CaseSerializer, SmallVariantQuerySerializer, SmallVariantQueryUpdateSerializer, SmallVariantForResultSerializer, + SettingsShortcutsSerializer, + SettingsShortcuts, ) @@ -192,7 +194,7 @@ class SmallVariantQueryRetrieveApiView(SmallVariantQueryApiMixin, RetrieveAPIVie """ -@attr.s(auto_attribs=True) +@attrs.define class JobStatus: status: typing.Optional[str] = None @@ -312,3 +314,161 @@ def get_queryset(self): def get_permission_required(self): return "variants.view_data" + + +class SmallVariantQuerySettingsShortcutApiView( + VariantsApiBaseMixin, RetrieveAPIView, +): + """ + Generate query settings for a given case by certain shortcuts. + + **URL:** ``/variants/api/query_case/settings-shortcut/{case.uuid}`` + + **Methods:** ``GET`` + + **Parameters:** + + - ``quick_preset`` - overall preset selection using the presets below, valid values are + + - ``defaults`` - applies presets that are recommended for starting out without a specific hypothesis + - ``de_novo`` - applies presets that are recommended for starting out when the hypothesis is dominannt + inheritance with *de novo* variants + - ``dominant`` - applies presets that are recommended for starting out when the hypothesis is dominant + inheritance (but not with *de novo* variants) + - ``homozygous_recessive`` - applies presets that are recommended for starting out when the hypothesis is + recessive with homzygous variants + - ``compound_heterozygous`` - applies presets that are recommended for starting out when the hypothesis is + recessive with compound heterozygous variants + - ``recessive`` - applies presets that are recommended for starting out when the hypothesis is recessive mode + of inheritance + - ``x_recessive`` - applies presets that are recommended for starting out when the hypothesis is X recessive + mode of inheritance + - ``clinvar_pathogenic`` - apply presets that are recommended for screening variants for known pathogenic + variants present Clinvar + - ``mitochondrial`` - apply presets recommended for starting out to filter for mitochondrial mode of + inheritance + - ``whole_exomes`` - apply presets that return all variants of the case, regardless of frequency, quality etc. + + - ``inheritance`` - preset selection for mode of inheritance, valid values are + + - ``any`` - no particular constraint on inheritance (default) + - ``dominant`` - allow variants compatible with dominant mode of inheritance (includes *de novo* variants) + - ``homozygous_recessive`` - allow variants compatible with homozygous recessive mode of inheritance + - ``compound_heterozygous`` - allow variants compatible with compound heterozygous recessive mode of + inheritance + - ``recessive`` - allow variants compatible with recessive mode of inheritance of a disease/trait (includes + both homozygous and compound heterozygous recessive) + - ``x_recessive`` - allow variants compatible with X_recessive mode of inheritance of a disease/trait + - ``mitochondrial`` - mitochondrial inheritance (also applicable for "clinvar pathogenic") + - ``custom`` - indicates custom settings such that none of the above inheritance settings applies + + - ``frequency`` - preset selection for frequencies, valid values are + + - ``dominant_super_strict`` - apply thresholds considered "very strict" in a dominant disease context + - ``dominant_strict`` - apply thresholds considered "strict" in a dominant disease context (default) + - ``dominant_relaxed`` - apply thresholds considered "relaxed" in a dominant disease context + - ``recessive_strict`` - apply thresholds considered "strict" in a recessiv disease context + - ``recessive_relaxed`` - apply thresholds considered "relaxed" in a recessiv disease context + - ``custom`` - indicates custom settings such that none of the above frequency settings applies + + - ``impact`` - preset selection for molecular impact values, valid values are + + - ``null_variant`` - allow variants that are predicted to be null variants + - ``aa_change_splicing`` - allow variants that are predicted to change the amino acid of the gene's + protein and also splicing variants + - ``all_coding_deep_intronic`` - allow all coding variants and also deeply intronic ones + - ``whole_transcript`` - allow variants from the whole transcript (exonic/intronic) + - ``any_impact`` - allow any predicted molecular impact + - ``custom`` - indicates custom settings such that none of the above impact settings applies + + - ``quality`` - preset selection for variant call quality values, valid values are + + - ``super_strict`` - very stricdt quality settings + - ``strict`` - strict quality settings, used as the default + - ``relaxed`` - relaxed quality settings + - ``any`` - ignore quality, all variants pass filter + - ``custom`` - indicates custom settings such that none of the above quality settings applies + + - ``chromosomes`` - preset selection for selecting chromosomes/regions/genes allow/block lists, valid values are + + - ``whole_genome`` - the defaults settings selecting the whole genome + - ``autosomes`` - select the variants lying on the autosomes only + - ``x_chromosome`` - select variants on the X chromosome only + - ``y_chromosome`` - select variants on the Y chromosome only + - ``mt_chromosome`` - select variants on the mitochondrial chromosome only + - ``custom`` - indicates custom settings such that none of the above chromosomes presets applies + + - ``flags_etc`` - preset selection for "flags etc." section, valid values are + + - ``defaults`` - the defaults also used in the user interface + - ``clinvar_only`` - select variants present in Clinvar only + - ``user_flagged`` - select user_flagged variants only + - ``custom`` - indicates custom settings such that none of the above flags etc. presets apply + + **Returns:** + + - ``presets`` - a ``dict`` with the following keys; this mirrors back the quick presets and further presets + selected in the parameters + + - ``quick_presets`` - one of the ``quick_presets`` preset values from above + - ``inheritance`` - one of the ``inheritance`` preset values from above + - ``frequency`` - one of the ``frequency`` preset values from above + - ``impact`` - one of the ``impact`` preset values from above + - ``quality`` - one of the ``quality`` preset values from above + - ``chromosomes`` - one of the ``chromosomes`` preset values from above + - ``flags_etc`` - one of the ``flags_etc`` preset values from above + + - ``query_settings`` - a ``dict`` with the query settings ready to be used for the given case; this will + follow :ref:`api_json_schemas_case_query_v1`. + + """ + + lookup_field = "sodar_uuid" + lookup_url_kwarg = "case" + renderer_classes = [VarfishApiRenderer] + versioning_class = VarfishApiVersioning + serializer_class = SettingsShortcutsSerializer + + def get_queryset(self): + return Case.objects.filter(project=self.get_project()) + + def get_object(self, *args, **kwargs): + quick_preset = self._get_quick_presets() + fields_dict = attrs.fields_dict(query_presets.QuickPresets) + changes_raw = {key: self.kwargs[key] for key in fields_dict if key in self.kwargs} + changes = {key: fields_dict[key].type[value] for key, value in changes_raw} + quick_preset = attrs.evolve(quick_preset, **changes) + return SettingsShortcuts( + presets={key: getattr(quick_preset, key).value for key in fields_dict}, + query_settings=cattr.unstructure( + quick_preset.to_settings(self._get_pedigree_members()) + ), + ) + + def _get_quick_presets(self) -> query_presets.QuickPresets: + """"Return quick preset if given in kwargs""" + if "quick_preset" in self.kwargs: + qp_name = self.kwargs["quick_preset"] + if qp_name not in attrs.fields_dict(query_presets._QuickPresetList): + raise NotFound(f"Could not find quick preset {qp_name}") + return getattr(query_presets.QUICK_PRESETS, qp_name) + else: + return query_presets.QUICK_PRESETS.defaults + + def _get_pedigree_members(self) -> typing.Tuple[query_presets.PedigreeMember]: + """Return pedigree members for the queried case.""" + case = self.get_queryset().get(sodar_uuid=self.kwargs["case"]) + return tuple( + query_presets.PedigreeMember( + family=None, + name=entry["patient"], + father=entry["father"], + mother=entry["mother"], + sex=query_presets.Sex(entry["sex"]), + disease_state=query_presets.DiseaseState(entry["affected"]), + ) + for entry in case.pedigree + ) + + def get_permission_required(self): + return "variants.view_data"