Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
bb814eb
fix(permissions): scope calibration-bearing reads to the requesting v…
bencap Aug 6, 2026
0825619
fix(permissions): withhold private calibrations from the experiment s…
bencap Aug 6, 2026
a5e8be8
fix(export): narrow the public dump without mutating ORM collections
bencap Aug 6, 2026
349576a
fix(export): key export loop on score set urn
bencap Aug 6, 2026
6b88a8f
fix(permissions): withhold community calibrations from score set owners
bencap Aug 6, 2026
275a3b6
fix(experiment-sets): narrow readable experiments without mutating th…
bencap Aug 6, 2026
1c3a199
fix(collections): check .permitted so permission filters take effect
bencap Aug 7, 2026
3b43d60
fix(annotation-tests): conditionally import logging context reliant a…
bencap Aug 10, 2026
b4a007b
refactor(score-sets): extract CSV export logic into score_set_csv module
bencap Aug 4, 2026
1493cc0
refactor(csv)!: replace score_set_csv with a namespaced CSV package
bencap Aug 6, 2026
39cfd21
feat(api): add variant-level clinical CSV and namespace discovery end…
bencap Aug 6, 2026
69bc2ea
feat(export): derive public dump namespaces from CSV discovery
bencap Aug 6, 2026
adfd630
refactor(csv): filter calibrations through ScoreCalibrationViewer
bencap Aug 6, 2026
1d1d36f
test(csv): cover the deprecated-param layer and collapse redundant nu…
bencap Aug 10, 2026
a895695
fix(csv): default to empty dict in variant csv rather than asserting
bencap Aug 10, 2026
3e222a2
fix(tests): add importorskips to relevant csv test modules
bencap Aug 11, 2026
7843dce
fix(csv): report GA4GH id instead of bare digest for post-mapped VRS
bencap Aug 12, 2026
aead12c
feat(csv): emit the whole gnomAD record in the gnomad namespace
bencap Aug 10, 2026
22f2299
fix(api): return uncaught-exception 500s through the CORS layer
bencap Aug 11, 2026
e1436cf
fix(api): report a failed variant in-band rather than truncating the …
bencap Aug 11, 2026
81536f7
test(annotation): assert emitted annotations round-trip, across a lis…
bencap Aug 11, 2026
51bb548
feat(scripts): sweep the annotation surfaces across the published corpus
bencap Aug 11, 2026
d955e68
fix(csv): stop an unparseable post-mapped payload from aborting a who…
bencap Aug 11, 2026
5921301
feat(scripts): sweep score-set CSV composition alongside the annotati…
bencap Aug 11, 2026
50c3137
fix(tests): import annotation utils from optional conftest utility
bencap Aug 11, 2026
c745403
fix(workflow): stop scheduling VEP annotation in the pipeline
bencap Aug 11, 2026
7fcf9a9
Merge pull request #840 from VariantEffect/bugfix/bencap/703/use-mapp…
bencap Aug 12, 2026
3b18b41
Merge pull request #821 from VariantEffect/bugfix/bencap/collection-m…
bencap Aug 12, 2026
c08d0c6
feat(scripts): add operator script for bulk score-set pipeline reruns
bencap Aug 12, 2026
ff25f3c
Merge pull request #841 from VariantEffect/feature/bencap/796/pipelin…
bencap Aug 12, 2026
1389e25
fix(tests): update test case with a valid ClinGen PA ID for no regist…
bencap Aug 12, 2026
3ec63d2
fix(tests): disable vep related job tests temporarily
bencap Aug 12, 2026
e34241b
chore(dependencies): bump pyasn1 to version 0.6.4 and soupsieve to ve…
bencap Aug 12, 2026
57ca54b
chore: bump version to 2026.2.7.1
bencap Aug 12, 2026
0a1fe2a
fix(csv): draw HGVS column names and null tokens from shared constants
bencap Aug 13, 2026
2d1beca
fix(export): carry every investigator score column in the public dump
bencap Aug 13, 2026
7951371
test(export): cover the public dump against its published README
bencap Aug 13, 2026
4e3c384
fix(tests): add conftest_optional import for core deps
bencap Aug 13, 2026
1118301
feat(scripts): cluster score-set pipeline cohorts by gene symbol
bencap Aug 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"

[tool.poetry]
name = "mavedb"
version = "2026.2.7"
version = "2026.2.7.1"
description = "API for MaveDB, the database of Multiplexed Assays of Variant Effect."
license = "AGPL-3.0-only"
readme = "README.md"
Expand Down
2 changes: 1 addition & 1 deletion src/mavedb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
logger = module_logging.getLogger(__name__)

__project__ = "mavedb-api"
__version__ = "2026.2.7"
__version__ = "2026.2.7.1"

logger.info(f"MaveDB {__version__}")

Expand Down
42 changes: 42 additions & 0 deletions src/mavedb/lib/acmg.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,48 @@
from mavedb.models.enums.strength_of_evidence import StrengthOfEvidenceProvided


def acmg_evidence_outcome_code(criterion: str, evidence_strength: Optional[str]) -> str:
"""Build the ACMG 2015 evidence outcome code for a criterion and the strength it was met at.

Three rules, which are the ACMG convention rather than anything MaveDB invented:

- no strength means the criterion was evaluated and *not* met, written ``"PS3_not_met"``
- STRONG is the criterion's baseline, so it is written bare: ``"PS3"``
- any other strength is suffixed: ``"PS3_moderate"``

Takes the criterion code and strength *name* as strings rather than enums so that the VA-Spec
annotation builders and the flat exports can share one implementation despite drawing their
enumerations from different places. That also means this survives any future decision about which
enumeration is canonical.

Parameters
----------
criterion : str
The criterion code, e.g. ``"PS3"`` or ``"BS3"``.
evidence_strength : Optional[str]
The strength name, e.g. ``"MODERATE"``. None when the criterion was not met.

Returns
-------
str
The evidence outcome code.

Examples
--------
>>> acmg_evidence_outcome_code("PS3", "STRONG")
'PS3'
>>> acmg_evidence_outcome_code("PS3", "MODERATE")
'PS3_moderate'
>>> acmg_evidence_outcome_code("BS3", None)
'BS3_not_met'
"""
if evidence_strength is None:
return f"{criterion}_not_met"
if evidence_strength.upper() == StrengthOfEvidenceProvided.STRONG.name:
return criterion
return f"{criterion}_{evidence_strength.lower()}"


def points_evidence_strength_equivalent(
points: int,
) -> tuple[Optional[ACMGCriterion], Optional[StrengthOfEvidenceProvided]]:
Expand Down
101 changes: 67 additions & 34 deletions src/mavedb/lib/annotation/annotate.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@
See: https://va-spec.ga4gh.org/en/latest/va-standard-profiles/community-profiles/acmg-2015-profiles.html#variant-pathogenicity-statement-acmg-2015
"""

from typing import Optional, Union
from typing import Optional, Sequence, TypeVar, Union

from ga4gh.va_spec.acmg_2015 import VariantPathogenicityStatement
from ga4gh.va_spec.base.core import ExperimentalVariantFunctionalImpactStudyResult, Statement

from mavedb.lib.annotation.classification import functional_classification_of_variant
from mavedb.lib.annotation.exceptions import MappingDataDoesntExistException
from mavedb.lib.annotation.evidence_line import acmg_evidence_line, functional_evidence_line
from mavedb.lib.annotation.exceptions import MappingDataDoesntExistException
from mavedb.lib.annotation.proposition import (
mapped_variant_to_experimental_variant_clinical_impact_proposition,
mapped_variant_to_experimental_variant_functional_impact_proposition,
Expand All @@ -26,39 +26,62 @@
)
from mavedb.lib.annotation.study_result import mapped_variant_to_experimental_variant_impact_study_result
from mavedb.lib.annotation.util import (
calibration_scope_extension,
calibrations_available_for_annotation,
can_annotate_variant_for_functional_statement,
can_annotate_variant_for_pathogenicity_evidence,
score_calibration_may_be_used_for_annotation,
select_strongest_functional_calibration,
select_strongest_pathogenicity_calibration,
)
from mavedb.lib.permissions.principal import Principal
from mavedb.models.mapped_variant import MappedVariant
from mavedb.models.score_calibration import ScoreCalibration

Annotation = TypeVar(
"Annotation", ExperimentalVariantFunctionalImpactStudyResult, Statement, VariantPathogenicityStatement
)


def _disclosing_calibration_scope(annotation: Annotation, calibrations: Sequence[ScoreCalibration]) -> Annotation:
"""Record on the annotation which principal it was built for.

Applied at the top-level entry points only. Nested study results and statements built as components of
an evidence line inherit the scope of the object that contains them.
"""
# model_copy rather than assigning to `.extensions`: mypy resolves the field's element type to a
# `ga4gh.va_spec.base.core.Extension` that does not exist at runtime (the ga4gh namespace packages
# confuse its import resolution), so a direct assignment is a false positive.
return annotation.model_copy(
update={"extensions": [*(annotation.extensions or []), calibration_scope_extension(calibrations)]}
)


def variant_study_result(mapped_variant: MappedVariant) -> ExperimentalVariantFunctionalImpactStudyResult:
return mapped_variant_to_experimental_variant_impact_study_result(mapped_variant)
# A study result reports the measured score and carries no calibration-derived evidence, so its scope
# is public regardless of viewer. Disclosed anyway, so that a missing scope never has to be read as
# "public" or "generated before disclosure existed".
return _disclosing_calibration_scope(mapped_variant_to_experimental_variant_impact_study_result(mapped_variant), [])


def variant_functional_impact_statement(
mapped_variant: MappedVariant, allow_research_use_only_calibrations: bool = False
mapped_variant: MappedVariant,
allow_research_use_only_calibrations: bool = False,
principal: Optional[Principal] = None,
) -> Optional[Statement]:
if not can_annotate_variant_for_functional_statement(
mapped_variant, allow_research_use_only_calibrations=allow_research_use_only_calibrations
mapped_variant, allow_research_use_only_calibrations=allow_research_use_only_calibrations, principal=principal
):
return None

study_result = mapped_variant_to_experimental_variant_impact_study_result(mapped_variant)
functional_proposition = mapped_variant_to_experimental_variant_functional_impact_proposition(mapped_variant)

# Collect eligible calibrations
eligible_calibrations = []
for score_calibration in mapped_variant.variant.score_set.score_calibrations:
if score_calibration_may_be_used_for_annotation(
score_calibration,
annotation_type="functional",
allow_research_use_only_calibrations=allow_research_use_only_calibrations,
):
eligible_calibrations.append(score_calibration)
eligible_calibrations = calibrations_available_for_annotation(
mapped_variant,
"functional",
allow_research_use_only_calibrations=allow_research_use_only_calibrations,
principal=principal,
)

# Select the calibration with the strongest evidence
strongest_calibration, strongest_range = select_strongest_functional_calibration(
Expand All @@ -77,32 +100,34 @@ def variant_functional_impact_statement(
for score_calibration in eligible_calibrations:
functional_evidence.append(functional_evidence_line(mapped_variant, score_calibration, [study_result]))

return mapped_variant_to_functional_statement(
mapped_variant, functional_proposition, functional_evidence, strongest_calibration, classification
return _disclosing_calibration_scope(
mapped_variant_to_functional_statement(
mapped_variant, functional_proposition, functional_evidence, strongest_calibration, classification
),
eligible_calibrations,
)


def variant_pathogenicity_statement(
mapped_variant: MappedVariant, allow_research_use_only_calibrations: bool = False
mapped_variant: MappedVariant,
allow_research_use_only_calibrations: bool = False,
principal: Optional[Principal] = None,
) -> Optional[VariantPathogenicityStatement]:
if not can_annotate_variant_for_pathogenicity_evidence(
mapped_variant, allow_research_use_only_calibrations=allow_research_use_only_calibrations
mapped_variant, allow_research_use_only_calibrations=allow_research_use_only_calibrations, principal=principal
):
return None

study_result = mapped_variant_to_experimental_variant_impact_study_result(mapped_variant)
functional_proposition = mapped_variant_to_experimental_variant_functional_impact_proposition(mapped_variant)
clinical_proposition = mapped_variant_to_experimental_variant_clinical_impact_proposition(mapped_variant)

# Collect eligible calibrations
eligible_calibrations = []
for score_calibration in mapped_variant.variant.score_set.score_calibrations:
if score_calibration_may_be_used_for_annotation(
score_calibration,
annotation_type="pathogenicity",
allow_research_use_only_calibrations=allow_research_use_only_calibrations,
):
eligible_calibrations.append(score_calibration)
eligible_calibrations = calibrations_available_for_annotation(
mapped_variant,
"pathogenicity",
allow_research_use_only_calibrations=allow_research_use_only_calibrations,
principal=principal,
)

# Select the calibration with the strongest evidence
strongest_calibration, strongest_range = select_strongest_pathogenicity_calibration(
Expand Down Expand Up @@ -130,25 +155,33 @@ def variant_pathogenicity_statement(
acmg_evidence_line(mapped_variant, score_calibration, clinical_proposition, [functional_statement])
)

return mapped_variant_to_pathogenicity_statement(
mapped_variant, clinical_proposition, clinical_evidence, strongest_calibration, strongest_range
return _disclosing_calibration_scope(
mapped_variant_to_pathogenicity_statement(
mapped_variant, clinical_proposition, clinical_evidence, strongest_calibration, strongest_range
),
eligible_calibrations,
)


def variant_highest_level_annotation(
mapped_variant: MappedVariant,
principal: Optional[Principal] = None,
) -> Optional[Union[ExperimentalVariantFunctionalImpactStudyResult, Statement, VariantPathogenicityStatement]]:
"""
Build the single highest-materialized VA-Spec layer for a mapped variant.

Layer ladder (highest to lowest): pathogenicity statement -> functional impact statement -> study result.
Returns None when the variant has no post-mapped allele and therefore cannot be annotated.

The viewer decides which layer is reachable as well as what the layer contains: a variant whose only
calibration is invisible to this principal degrades to a study result rather than yielding a statement
with nothing in it.
"""
try:
if can_annotate_variant_for_pathogenicity_evidence(mapped_variant):
return variant_pathogenicity_statement(mapped_variant)
if can_annotate_variant_for_functional_statement(mapped_variant):
return variant_functional_impact_statement(mapped_variant)
if can_annotate_variant_for_pathogenicity_evidence(mapped_variant, principal=principal):
return variant_pathogenicity_statement(mapped_variant, principal=principal)
if can_annotate_variant_for_functional_statement(mapped_variant, principal=principal):
return variant_functional_impact_statement(mapped_variant, principal=principal)
return variant_study_result(mapped_variant)
except MappingDataDoesntExistException:
return None
35 changes: 26 additions & 9 deletions src/mavedb/lib/annotation/classification.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,35 @@ class ExperimentalVariantFunctionalImpactClassification(StrEnum):
INDETERMINATE = "indeterminate"


def _classification_contains_variant(
functional_classification: ScoreCalibrationFunctionalClassification,
mapped_variant: MappedVariant,
containing_classification_ids: Optional[set[int]],
) -> bool:
"""Whether this classification's score range contains the variant.

Prefers a pre-resolved id set, which is an O(1) check. Falls back to the ORM relationship, which is
correct but loads every variant of the range.
"""
if containing_classification_ids is not None:
return functional_classification.id in containing_classification_ids
return mapped_variant.variant in functional_classification.variants


def functional_classification_of_variant(
mapped_variant: MappedVariant, score_calibration: ScoreCalibration
mapped_variant: MappedVariant,
score_calibration: ScoreCalibration,
containing_classification_ids: Optional[set[int]] = None,
) -> tuple[Optional[ScoreCalibrationFunctionalClassification], ExperimentalVariantFunctionalImpactClassification]:
"""Classify a variant's functional impact as normal, abnormal, or indeterminate.

Uses the primary score calibration and its functional ranges.
Raises ValueError if required calibration or score is missing.

*containing_classification_ids*, when given, is the set of functional-classification ids already known
to contain this variant. Pass it to avoid the ORM membership check below, which loads every variant of
every range. A caller classifying many variants should resolve membership once from the association
table; see ``mavedb.lib.csv.variant``.
"""
if not mapped_variant.variant.score_set.score_calibrations:
raise ValueError(
Expand All @@ -41,11 +63,8 @@ def functional_classification_of_variant(
" Unable to classify functional impact."
)

# TODO#XXX: Performance: avoid ORM relationship membership checks (`variant in functional_range.variants`) in this
# DB-agnostic function. Resolve class-based matches in an upstream DB-aware layer using the association table,
# pass matched functional classification IDs into this function, and use O(1) ID membership checks here.
for functional_range in score_calibration.functional_classifications:
if mapped_variant.variant in functional_range.variants:
if _classification_contains_variant(functional_range, mapped_variant, containing_classification_ids):
if functional_range.functional_classification is FunctionalClassificationOptions.normal:
return functional_range, ExperimentalVariantFunctionalImpactClassification.NORMAL
elif functional_range.functional_classification is FunctionalClassificationOptions.abnormal:
Expand All @@ -58,6 +77,7 @@ def functional_classification_of_variant(
def pathogenicity_classification_of_variant(
mapped_variant: MappedVariant,
score_calibration: ScoreCalibration,
containing_classification_ids: Optional[set[int]] = None,
) -> tuple[
Optional[ScoreCalibrationFunctionalClassification],
VariantPathogenicityEvidenceLine.Criterion,
Expand Down Expand Up @@ -87,11 +107,8 @@ def pathogenicity_classification_of_variant(
" Unable to classify clinical impact."
)

# TODO#XXX: Performance: avoid ORM relationship membership checks (`variant in pathogenicity_range.variants`) in this
# DB-agnostic function. Resolve class-based matches in an upstream DB-aware layer using the association table,
# pass matched functional classification IDs into this function, and use O(1) ID membership checks here.
for pathogenicity_range in score_calibration.functional_classifications:
if mapped_variant.variant in pathogenicity_range.variants:
if _classification_contains_variant(pathogenicity_range, mapped_variant, containing_classification_ids):
if pathogenicity_range.acmg_classification is None:
return (pathogenicity_range, VariantPathogenicityEvidenceLine.Criterion.PS3, None)

Expand Down
Loading
Loading