From e1d23cd61a53ac24829336c1614184937842d87f Mon Sep 17 00:00:00 2001 From: Dave Lawrence Date: Sun, 30 Aug 2026 23:55:07 +0000 Subject: [PATCH] Remove duplicate implementations in the classification app #1796 - delete evidence_key_cleaner: an identical copy of unknown_evidence_key_cleaner, whose only extra argument was never read and made argparse raise at add_argument - delete Classification.get_allele_info_dict: dead since classification_json took over, and had missed the g.HGVS fallback (#1063) and variant_coordinate since - pull the met-criteria + AMP level walk up into EvidenceMixin.met_criteria_strengths - share the discordance movement guards between _less_more_certain and _up_down_for --- classification/admin/classification_admin.py | 36 ++++++-------- .../commands/evidence_key_cleaner.py | 49 ------------------- classification/models/classification.py | 41 ---------------- .../models/classification_groups.py | 22 +-------- classification/models/evidence_mixin.py | 20 +++++++- .../models/evidence_mixin_summary_cache.py | 21 +------- 6 files changed, 39 insertions(+), 150 deletions(-) delete mode 100644 classification/management/commands/evidence_key_cleaner.py diff --git a/classification/admin/classification_admin.py b/classification/admin/classification_admin.py index 91b102ab2..5192a6661 100644 --- a/classification/admin/classification_admin.py +++ b/classification/admin/classification_admin.py @@ -1,4 +1,5 @@ import json +from collections.abc import Callable from datetime import timedelta from typing import Optional, Union @@ -738,7 +739,9 @@ def cs_to_index(): return EvidenceKeyMap.cached_key(SpecialEKeys.CLINICAL_SIGNIFICANCE).option_dictionary_property("vg") @staticmethod - def _less_more_certain(summary: DiscordanceLabSummary): + def _describe_movement(summary: DiscordanceLabSummary, describe: Callable[[int, int], str]) -> str: + """ Handles the cases where the two clinical significances can't be compared (withdrawn, unchanged, + or one of them not on the vg scale), otherwise describe() names the direction travelled """ cs_to_index = DiscordanceReportAdminExport.cs_to_index() from_value = int(cs_to_index.get(summary.clinical_significance_from, "0")) to_value = int(cs_to_index.get(summary.clinical_significance_to, "0")) @@ -749,29 +752,22 @@ def _less_more_certain(summary: DiscordanceLabSummary): return "same" elif from_value == 0 or to_value == 0: return "?" - else: - if abs(to_value - 3) > abs(from_value - 3): - return "more" - else: - return "less" + return describe(from_value, to_value) + + @staticmethod + def _less_more_certain(summary: DiscordanceLabSummary): + # VUS is the middle of the vg scale, so distance from it is how certain the call is + def describe(from_value: int, to_value: int) -> str: + return "more" if abs(to_value - 3) > abs(from_value - 3) else "less" + + return DiscordanceReportAdminExport._describe_movement(summary, describe) @staticmethod def _up_down_for(summary: DiscordanceLabSummary): - cs_to_index = DiscordanceReportAdminExport.cs_to_index() - from_value = int(cs_to_index.get(summary.clinical_significance_from, "0")) - to_value = int(cs_to_index.get(summary.clinical_significance_to, "0")) + def describe(from_value: int, to_value: int) -> str: + return "upgrade" if to_value > from_value else "downgrade" - if summary.clinical_significance_to == 'withdrawn': - return "withdrawn" - elif from_value == to_value: - return "same" - elif from_value == 0 or to_value == 0: - return "?" - else: - if to_value > from_value: - return "upgrade" - else: - return "downgrade" + return DiscordanceReportAdminExport._describe_movement(summary, describe) def __init__(self, discordance_report: DiscordanceReport, perspective: LabPickerData): self.discordance_report = discordance_report diff --git a/classification/management/commands/evidence_key_cleaner.py b/classification/management/commands/evidence_key_cleaner.py deleted file mode 100644 index a6de389be..000000000 --- a/classification/management/commands/evidence_key_cleaner.py +++ /dev/null @@ -1,49 +0,0 @@ -from typing import Optional - -from django.core.management import BaseCommand - -from classification.enums import SubmissionSource -from classification.models import ClassificationModification, EvidenceKeyMap -from library.guardian_utils import admin_bot -from snpdb.models import Lab - - -class Command(BaseCommand): - - def add_arguments(self, parser): - parser.add_argument('--key', type='str', required=True) - parser.add_argument('--lab', type=str, required=False) - parser.add_argument('--apply', action="store_true") - - def handle(self, *args, **options): - lab_id: str = options["lab"] - lab: Optional[Lab] = None - if lab_id: - if lab_id.isnumeric(): - lab = Lab.objects.get(id=lab_id) - else: - lab = Lab.objects.get(group_name=lab_id) - print(f"Lab = {lab!s}") - - qs = ClassificationModification.objects.filter(is_last_published=True, is_last_edited=True) - if lab: - qs = qs.filter(classification__lab=lab) - - valid_evidence_keys = set(k.key for k in EvidenceKeyMap.cached().all_keys) - - apply = options["apply"] - - mod: ClassificationModification - for mod in qs.iterator(): - if bonus_keys := set(mod.published_evidence.keys()).difference(valid_evidence_keys): - print(f"{mod.classification.pk} has illegal evidence keys = {bonus_keys}") - - if apply: - patch = dict.fromkeys(bonus_keys) - classification = mod.classification - classification.patch_value(patch=patch, - source=SubmissionSource.API, - save=True, - user=admin_bot()) - classification.publish_latest(user=admin_bot()) - print("Updated") diff --git a/classification/models/classification.py b/classification/models/classification.py index 81f3a9e18..7e19e2972 100644 --- a/classification/models/classification.py +++ b/classification/models/classification.py @@ -2028,47 +2028,6 @@ def get_visible_evidence(self, evidence, lowest_share_level: ShareLevel) -> dict visible_evidence[k] = {'value': "(hidden)", 'hidden': True} return visible_evidence - def get_allele_info_dict(self) -> Optional[dict[str, Any]]: - allele_info_dict = {} - if allele_info := self.allele_info: - resolved_dict = { - "allele_id": allele_info.allele_id, - "allele_info_id": allele_info.id, - "allele_info_status": allele_info.status, - "status": allele_info.status, - "include": allele_info.latest_validation.include if allele_info.latest_validation else None, - "variant_coordinate": allele_info.variant_coordinate - } - - if (genome_build := self.get_genome_build_opt()) and \ - (preferred_build := allele_info[genome_build]) and \ - (c_hgvs := preferred_build.c_hgvs_display): - resolved_dict.update(c_hgvs.to_json()) - elif c_hgvs_raw := self.get(SpecialEKeys.C_HGVS): - resolved_dict.update(HGVSDisplay.parse(c_hgvs_raw).to_json()) - - include = False - if latest_validation := allele_info.latest_validation: - include = latest_validation.include - - resolved_dict["include"] = include - if warning_icon := ImportedAlleleInfo.icon_for(status=allele_info.status, include=include): - resolved_dict.update(warning_icon.as_json()) - - allele_info_dict["resolved"] = resolved_dict - - genome_builds = {} - for variant_info in allele_info.resolved_builds: - genome_builds[variant_info.genome_build.name] = { - 'variant_id': variant_info.variant_id, - SpecialEKeys.C_HGVS: variant_info.c_hgvs - } - - if genome_builds: - allele_info_dict["genome_builds"] = genome_builds - - return allele_info_dict - @staticmethod def get_url_for_pk(pk): return reverse('view_classification', kwargs={'classification_id': pk}) diff --git a/classification/models/classification_groups.py b/classification/models/classification_groups.py index 05e1889b1..b7d986502 100644 --- a/classification/models/classification_groups.py +++ b/classification/models/classification_groups.py @@ -9,7 +9,7 @@ from more_itertools import first from classification.criteria_strengths import CriteriaStrength -from classification.enums import CriteriaEvaluation, ShareLevel, SpecialEKeys +from classification.enums import ShareLevel, SpecialEKeys from classification.models import ( ClassificationModification, ConditionResolved, @@ -371,25 +371,7 @@ def count(self) -> int: @cached_property def acmg_criteria(self) -> MultiValues[CriteriaStrength]: - - def criteria_converter(cm: ClassificationModification) -> set[CriteriaStrength]: - strengths: set[CriteriaStrength] = set() - for e_key in EvidenceKeyMap.cached().criteria(): - strength = cm.get(e_key.key) - if CriteriaEvaluation.is_met(strength): - strengths.add(CriteriaStrength(e_key, strength)) - for amp_level, letter in SpecialEKeys.AMP_LEVELS_TO_LEVEL.items(): - if value := cm.get_value_list(amp_level): - e_key = EvidenceKeyMap.cached_key(amp_level) - for sub_value in value: - sub_value_label = e_key.pretty_value(sub_value) - strengths.add(CriteriaStrength( - ekey=EvidenceKeyMap.cached_key(amp_level), - custom_strength=f"{letter}_{sub_value_label}") - ) - return strengths - output = MultiValues.convert([criteria_converter(cm) for cm in self.modifications]) - return output + return MultiValues.convert([cm.met_criteria_strengths() for cm in self.modifications]) def _evidence_key_set(self, key: str) -> list[str]: all_values = set() diff --git a/classification/models/evidence_mixin.py b/classification/models/evidence_mixin.py index f331b7b6d..9c34ddaa1 100644 --- a/classification/models/evidence_mixin.py +++ b/classification/models/evidence_mixin.py @@ -9,7 +9,7 @@ from annotation.models import CitationFetchRequest from annotation.models.models_citations import CitationFetchResponse from classification.criteria_strengths import CriteriaStrength, CriteriaStrengths -from classification.enums import AlleleOriginBucket, SpecialEKeys +from classification.enums import AlleleOriginBucket, CriteriaEvaluation, SpecialEKeys from genes.hgvs import HGVSComponents, PHGVS from library.log_utils import report_message from library.utils import empty_to_none @@ -221,6 +221,24 @@ def criteria_strengths(self, e_keys: Optional['EvidenceKeyMap'] = None) -> Crite return CriteriaStrengths(strengths=criteria, is_acmg_standard=self.is_likely_acmg) + def met_criteria_strengths(self) -> set[CriteriaStrength]: + """ Criteria that were actually met, plus one per somatic AMP level value selected """ + from classification.models import EvidenceKeyMap + + strengths: set[CriteriaStrength] = set() + for e_key in EvidenceKeyMap.cached().criteria(): + strength = self.get(e_key.key) + if CriteriaEvaluation.is_met(strength): + strengths.add(CriteriaStrength(e_key, strength)) + for amp_level, letter in SpecialEKeys.AMP_LEVELS_TO_LEVEL.items(): + if value := self.get_value_list(amp_level): + e_key = EvidenceKeyMap.cached_key(amp_level) + for sub_value in value: + sub_value_label = e_key.pretty_value(sub_value) + strengths.add(CriteriaStrength(ekey=e_key, + custom_strength=f"{letter}_{sub_value_label}")) + return strengths + def criteria_strength_summary(self, ekeys: Optional['EvidenceKeyMap'] = None, only_acmg: bool = False) -> str: strengths = self.criteria_strengths(e_keys=ekeys) return strengths.summary_string(acmg_only=only_acmg) diff --git a/classification/models/evidence_mixin_summary_cache.py b/classification/models/evidence_mixin_summary_cache.py index fd1f87bb8..5e0dcc229 100644 --- a/classification/models/evidence_mixin_summary_cache.py +++ b/classification/models/evidence_mixin_summary_cache.py @@ -2,8 +2,7 @@ from functools import cached_property from typing import Optional, Self, TypedDict -from classification.criteria_strengths import CriteriaStrength -from classification.enums import AlleleOriginBucket, CriteriaEvaluation, SpecialEKeys +from classification.enums import AlleleOriginBucket, SpecialEKeys from classification.models.evidence_key import EvidenceKeyMap from library.utils import strip_json @@ -207,20 +206,4 @@ def somatic_sort(self) -> Optional[int]: @cached_property def criteria_labels(self) -> list[str]: - from classification.models import EvidenceKeyMap - cm = self.cm - strengths: set[CriteriaStrength] = set() - for e_key in EvidenceKeyMap.cached().criteria(): - strength = cm.get(e_key.key) - if CriteriaEvaluation.is_met(strength): - strengths.add(CriteriaStrength(e_key, strength)) - for amp_level, letter in SpecialEKeys.AMP_LEVELS_TO_LEVEL.items(): - if value := cm.get_value_list(amp_level): - e_key = EvidenceKeyMap.cached_key(amp_level) - for sub_value in value: - sub_value_label = e_key.pretty_value(sub_value) - strengths.add(CriteriaStrength( - ekey=EvidenceKeyMap.cached_key(amp_level), - custom_strength=f"{letter}_{sub_value_label}") - ) - return list(str(x) for x in sorted(strengths)) + return [str(x) for x in sorted(self.cm.met_criteria_strengths())]