Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3,472 changes: 1,752 additions & 1,720 deletions poetry.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[build-system]
requires = ["poetry-core"]
requires = ["setuptools", "poetry-core"]
build-backend = "poetry.core.masonry.api"

[tool.poetry]
Expand Down Expand Up @@ -33,7 +33,7 @@ eutils = "~0.6.0"
email_validator = "~2.1.1"
numpy = "~1.26"
httpx = "~0.26.0"
pandas = "~1.4.1"
pandas = ">=2.2.0,<3.0.0"
pydantic = "~2.10.0"
python-dotenv = "~0.20.0"
python-json-logger = "~2.0.7"
Expand Down
4 changes: 2 additions & 2 deletions src/mavedb/logging/config.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import os
from importlib.resources import files

import yaml
from pkg_resources import resource_stream


def load_stock_config(name="default"):
"""
Loads a built-in stock logging configuration based on *name*.
"""
with resource_stream(__package__, f"configurations/{name}.yaml") as file:
with files(__package__).joinpath(f"configurations/{name}.yaml").open("r") as file:
return load_config(file)


Expand Down
15 changes: 13 additions & 2 deletions src/mavedb/models/score_calibration_functional_classification.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

from typing import TYPE_CHECKING

from sqlalchemy import Boolean, Column, Enum, Float, ForeignKey, Integer, String
from sqlalchemy import Boolean, Column, Enum, Float, ForeignKey, Integer, String, func, select
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, relationship
from sqlalchemy.orm import Mapped, column_property, relationship

from mavedb.db.base import Base
from mavedb.lib.validation.utilities import inf_or_float
Expand Down Expand Up @@ -58,6 +58,17 @@ class ScoreCalibrationFunctionalClassification(Base):
secondary=score_calibration_functional_classification_variants_association_table,
)

# Efficient count via correlated subquery — avoids loading all variant objects.
variant_count: Mapped[int] = column_property(
select(func.count())
.where(
score_calibration_functional_classification_variants_association_table.c.functional_classification_id
== id # refers to the `id` Column defined above
)
.correlate_except(score_calibration_functional_classification_variants_association_table)
.scalar_subquery()
)

def score_is_contained_in_range(self, score: float) -> bool:
"""Check if a given score falls within the defined range."""
if self.range is None or not isinstance(self.range, list) or len(self.range) != 2:
Expand Down
95 changes: 95 additions & 0 deletions src/mavedb/routers/score_calibrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from mavedb.lib.validation.dataframe.calibration import validate_and_standardize_calibration_classes_dataframe
from mavedb.lib.validation.exceptions import ValidationError
from mavedb.models.score_calibration import ScoreCalibration
from mavedb.models.score_calibration_functional_classification import ScoreCalibrationFunctionalClassification
from mavedb.models.score_set import ScoreSet
from mavedb.view_models import score_calibration

Expand Down Expand Up @@ -684,3 +685,97 @@ def publish_score_calibration_route(
db.refresh(item)

return item


@router.get(
"/{urn}/functional-classifications/{classification_id}/variants",
response_model=score_calibration.FunctionalClassificationVariants,
responses={404: {}},
)
def get_functional_classification_variants(
*,
urn: str,
classification_id: int,
db: Session = Depends(deps.get_db),
user_data: Optional[UserData] = Depends(get_current_user),
) -> score_calibration.FunctionalClassificationVariants:
"""
Retrieve variants for a specific functional classification within a score calibration.

Returns the list of variants whose scores fall within the functional classification's
defined range or class. Use this endpoint when you need the full variant data for a
specific classification — the main score set and calibration endpoints return only
a `variant_count` summary for performance.
"""
save_to_logging_context(
{"requested_resource": urn, "requested_classification": classification_id, "resource_property": "variants"}
)

calibration = (
db.query(ScoreCalibration)
.options(selectinload(ScoreCalibration.score_set).selectinload(ScoreSet.contributors))
.where(ScoreCalibration.urn == urn)
.one_or_none()
)
if not calibration:
logger.debug("The requested score calibration does not exist", extra=logging_context())
raise HTTPException(status_code=404, detail="The requested score calibration does not exist")

assert_permission(user_data, calibration, Action.READ)

functional_classification = (
db.query(ScoreCalibrationFunctionalClassification)
.filter(
ScoreCalibrationFunctionalClassification.id == classification_id,
ScoreCalibrationFunctionalClassification.calibration_id == calibration.id,
)
.one_or_none()
)
if not functional_classification:
logger.debug("The requested functional classification does not exist", extra=logging_context())
raise HTTPException(status_code=404, detail="The requested functional classification does not exist")

return score_calibration.FunctionalClassificationVariants(
functional_classification_id=functional_classification.id, variants=functional_classification.variants
)


@router.get(
"/{urn}/variants",
response_model=list[score_calibration.FunctionalClassificationVariants],
responses={404: {}},
)
def get_calibration_all_variants(
*,
urn: str,
db: Session = Depends(deps.get_db),
user_data: Optional[UserData] = Depends(get_current_user),
) -> list[score_calibration.FunctionalClassificationVariants]:
"""
Retrieve all variants across all functional classifications for a score calibration.

Returns a list of variant sets, one per functional classification. Use this endpoint
when you need the full variant data for an entire calibration — the main score set and
calibration endpoints return only a `variant_count` summary for performance.
"""
save_to_logging_context({"requested_resource": urn, "resource_property": "variants"})

calibration = (
db.query(ScoreCalibration)
.options(selectinload(ScoreCalibration.score_set).selectinload(ScoreSet.contributors))
.where(ScoreCalibration.urn == urn)
.one_or_none()
)
if not calibration:
logger.debug("The requested score calibration does not exist", extra=logging_context())
raise HTTPException(status_code=404, detail="The requested score calibration does not exist")

assert_permission(user_data, calibration, Action.READ)

results = []
for fc in calibration.functional_classifications:
results.append(
score_calibration.FunctionalClassificationVariants(functional_classification_id=fc.id, variants=fc.variants)
)

return results
14 changes: 9 additions & 5 deletions src/mavedb/view_models/score_calibration.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,7 @@
from mavedb.view_models.user import SavedUser, User

if TYPE_CHECKING:
from mavedb.view_models.variant import (
SavedVariantEffectMeasurement,
VariantEffectMeasurement,
)
from mavedb.view_models.variant import VariantEffectMeasurement

### Functional range models

Expand Down Expand Up @@ -242,9 +239,10 @@ class FunctionalClassificationCreate(FunctionalClassificationModify):
class SavedFunctionalClassification(FunctionalClassificationBase):
"""Persisted functional range model (includes record type metadata)."""

id: int
record_type: str = None # type: ignore
acmg_classification: Optional[SavedACMGClassification] = None
variants: Sequence["SavedVariantEffectMeasurement"] = []
variant_count: int = 0

_record_type_factory = record_type_validator()(set_record_type)

Expand All @@ -259,6 +257,12 @@ class FunctionalClassification(SavedFunctionalClassification):
"""Complete functional range model returned by the API."""

acmg_classification: Optional[ACMGClassification] = None


class FunctionalClassificationVariants(BaseModel):
"""Response model for functional classification variant endpoints."""

functional_classification_id: int
variants: Sequence["VariantEffectMeasurement"] = []


Expand Down
18 changes: 12 additions & 6 deletions tests/helpers/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -1444,10 +1444,11 @@


TEST_SAVED_FUNCTIONAL_RANGE_NORMAL = {
"id": 1,
"recordType": "FunctionalClassification",
**{camelize(k): v for k, v in TEST_FUNCTIONAL_RANGE_NORMAL.items() if k not in ("acmg_classification",)},
"acmgClassification": TEST_SAVED_ACMG_BS3_STRONG_CLASSIFICATION,
"variants": [],
"variantCount": 0,
}


Expand All @@ -1464,10 +1465,11 @@


TEST_SAVED_FUNCTIONAL_RANGE_ABNORMAL = {
"id": 2,
"recordType": "FunctionalClassification",
**{camelize(k): v for k, v in TEST_FUNCTIONAL_RANGE_ABNORMAL.items() if k not in ("acmg_classification",)},
"acmgClassification": TEST_SAVED_ACMG_PS3_STRONG_CLASSIFICATION,
"variants": [],
"variantCount": 0,
}


Expand All @@ -1481,9 +1483,10 @@


TEST_SAVED_FUNCTIONAL_RANGE_NOT_SPECIFIED = {
"id": 3,
"recordType": "FunctionalClassification",
**{camelize(k): v for k, v in TEST_FUNCTIONAL_RANGE_NOT_SPECIFIED.items()},
"variants": [],
"variantCount": 0,
}


Expand All @@ -1498,10 +1501,11 @@


TEST_SAVED_FUNCTIONAL_CLASSIFICATION_NORMAL = {
"id": 1,
"recordType": "FunctionalClassification",
**{camelize(k): v for k, v in TEST_FUNCTIONAL_CLASSIFICATION_NORMAL.items() if k not in ("acmg_classification",)},
"acmgClassification": TEST_SAVED_ACMG_BS3_STRONG_CLASSIFICATION,
"variants": [],
"variantCount": 0,
}


Expand All @@ -1516,10 +1520,11 @@


TEST_SAVED_FUNCTIONAL_CLASSIFICATION_ABNORMAL = {
"id": 2,
"recordType": "FunctionalClassification",
**{camelize(k): v for k, v in TEST_FUNCTIONAL_CLASSIFICATION_ABNORMAL.items() if k not in ("acmg_classification",)},
"acmgClassification": TEST_SAVED_ACMG_PS3_STRONG_CLASSIFICATION,
"variants": [],
"variantCount": 0,
}


Expand All @@ -1531,9 +1536,10 @@


TEST_SAVED_FUNCTIONAL_CLASSIFICATION_NOT_SPECIFIED = {
"id": 3,
"recordType": "FunctionalClassification",
**{camelize(k): v for k, v in TEST_FUNCTIONAL_CLASSIFICATION_NOT_SPECIFIED.items()},
"variants": [],
"variantCount": 0,
}


Expand Down
Loading