Skip to content

Commit 01e3db1

Browse files
committed
Merge branch 'release-2025.5.0' of https://github.com/VariantEffect/mavedb-api into maintenance/bencap/542/cleanup-open-api-docs
2 parents 9f327f3 + 7b4a20c commit 01e3db1

File tree

90 files changed

+12808
-4027
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

90 files changed

+12808
-4027
lines changed

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# python-base
33
# Set up shared environment variables
44
################################
5-
FROM --platform=amd64 python:3.11 AS python-base
5+
FROM python:3.11 AS python-base
66

77
# Poetry
88
# https://python-poetry.org/docs/configuration/#using-environment-variables
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
2+
from typing import Union
3+
4+
import sqlalchemy as sa
5+
from sqlalchemy.orm import Session
6+
7+
# SQLAlchemy needs access to all models to properly map relationships.
8+
from mavedb.models import *
9+
10+
from mavedb.db.session import SessionLocal
11+
from mavedb.models.score_set import ScoreSet
12+
from mavedb.models.score_calibration import ScoreCalibration as ScoreCalibrationDBModel
13+
from mavedb.models.publication_identifier import PublicationIdentifier
14+
from mavedb.view_models.score_range import (
15+
ScoreRangeCreate,
16+
ScoreSetRangesAdminCreate,
17+
ZeibergCalibrationScoreRangesAdminCreate,
18+
ScottScoreRangesAdminCreate,
19+
InvestigatorScoreRangesAdminCreate,
20+
IGVFCodingVariantFocusGroupControlScoreRangesAdminCreate,
21+
IGVFCodingVariantFocusGroupMissenseScoreRangesAdminCreate,
22+
)
23+
24+
score_range_kinds: dict[
25+
str,
26+
Union[
27+
type[ZeibergCalibrationScoreRangesAdminCreate],
28+
type[ScottScoreRangesAdminCreate],
29+
type[InvestigatorScoreRangesAdminCreate],
30+
type[IGVFCodingVariantFocusGroupControlScoreRangesAdminCreate],
31+
type[IGVFCodingVariantFocusGroupMissenseScoreRangesAdminCreate],
32+
],
33+
] = {
34+
"zeiberg_calibration": ZeibergCalibrationScoreRangesAdminCreate,
35+
"scott_calibration": ScottScoreRangesAdminCreate,
36+
"investigator_provided": InvestigatorScoreRangesAdminCreate,
37+
"cvfg_all_variants": IGVFCodingVariantFocusGroupControlScoreRangesAdminCreate,
38+
"cvfg_missense_variants": IGVFCodingVariantFocusGroupMissenseScoreRangesAdminCreate,
39+
}
40+
41+
EVIDENCE_STRENGTH_FROM_POINTS = {
42+
8: "Very Strong",
43+
4: "Strong",
44+
3: "Moderate+",
45+
2: "Moderate",
46+
1: "Supporting",
47+
}
48+
49+
50+
def do_migration(session: Session) -> None:
51+
score_sets_with_ranges = (
52+
session.execute(sa.select(ScoreSet).where(ScoreSet.score_ranges.isnot(None))).scalars().all()
53+
)
54+
55+
for score_set in score_sets_with_ranges:
56+
if not score_set.score_ranges:
57+
continue
58+
59+
score_set_ranges = ScoreSetRangesAdminCreate.model_validate(score_set.score_ranges)
60+
61+
for field in score_set_ranges.model_fields_set:
62+
if field == "record_type":
63+
continue
64+
65+
ranges = getattr(score_set_ranges, field)
66+
if not ranges:
67+
continue
68+
69+
range_model = score_range_kinds.get(field)
70+
inferred_ranges = range_model.model_validate(ranges)
71+
72+
model_thresholds = []
73+
for range in inferred_ranges.ranges:
74+
model_thresholds.append(ScoreRangeCreate.model_validate(range.__dict__).model_dump())
75+
76+
# We should migrate the zeiberg evidence classifications to be explicitly part of the calibration ranges.
77+
if field == "zeiberg_calibration":
78+
for inferred_range, model_range in zip(
79+
inferred_ranges.ranges,
80+
model_thresholds,
81+
):
82+
model_range["label"] = f"PS3 {EVIDENCE_STRENGTH_FROM_POINTS.get(inferred_range.evidence_strength, 'Unknown')}" if inferred_range.evidence_strength > 0 else f"BS3 {EVIDENCE_STRENGTH_FROM_POINTS.get(abs(inferred_range.evidence_strength), 'Unknown')}"
83+
model_range["acmg_classification"] = {"points": inferred_range.evidence_strength}
84+
85+
# Reliant on existing behavior that these sources have been created already.
86+
# If not present, no sources will be associated.
87+
if "odds_path_source" in inferred_ranges.model_fields_set and inferred_ranges.odds_path_source:
88+
oddspaths_sources = (
89+
session.execute(
90+
sa.select(PublicationIdentifier).where(
91+
PublicationIdentifier.identifier.in_(
92+
[src.identifier for src in (inferred_ranges.odds_path_source or [])]
93+
)
94+
)
95+
)
96+
.scalars()
97+
.all()
98+
)
99+
else:
100+
oddspaths_sources = []
101+
102+
if "source" in inferred_ranges.model_fields_set and inferred_ranges.source:
103+
range_sources = (
104+
session.execute(
105+
sa.select(PublicationIdentifier).where(
106+
PublicationIdentifier.identifier.in_(
107+
[src.identifier for src in (inferred_ranges.source or [])]
108+
)
109+
)
110+
)
111+
.scalars()
112+
.all()
113+
)
114+
else:
115+
range_sources = []
116+
117+
sources = set()
118+
for publication in oddspaths_sources:
119+
setattr(publication, "relation", "method")
120+
sources.add(publication)
121+
for publication in range_sources:
122+
setattr(publication, "relation", "threshold")
123+
sources.add(publication)
124+
125+
score_calibration = ScoreCalibrationDBModel(
126+
score_set_id=score_set.id,
127+
title=inferred_ranges.title,
128+
research_use_only=inferred_ranges.research_use_only,
129+
primary=inferred_ranges.primary,
130+
private=False, # All migrated calibrations are public.
131+
investigator_provided=True if field == "investigator_provided" else False,
132+
baseline_score=inferred_ranges.baseline_score
133+
if "baseline_score" in inferred_ranges.model_fields_set
134+
else None,
135+
baseline_score_description=inferred_ranges.baseline_score_description
136+
if "baseline_score_description" in inferred_ranges.model_fields_set
137+
else None,
138+
functional_ranges=None if not model_thresholds else model_thresholds,
139+
calibration_metadata=None,
140+
publication_identifiers=sources,
141+
# If investigator_provided, set to creator of score set, else set to default system user (1).
142+
created_by_id=score_set.created_by_id if field == "investigator_provided" else 1,
143+
modified_by_id=score_set.created_by_id if field == "investigator_provided" else 1,
144+
)
145+
session.add(score_calibration)
146+
147+
148+
if __name__ == "__main__":
149+
db = SessionLocal()
150+
db.current_user = None # type: ignore
151+
152+
do_migration(db)
153+
154+
db.commit()
155+
db.close()
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
"""add score calibration table
2+
3+
Revision ID: 002f6f9ec7ac
4+
Revises: 019eb75ad9ae
5+
Create Date: 2025-10-08 08:59:10.563528
6+
7+
"""
8+
9+
from alembic import op
10+
import sqlalchemy as sa
11+
from sqlalchemy.dialects import postgresql
12+
13+
14+
# revision identifiers, used by Alembic.
15+
revision = "002f6f9ec7ac"
16+
down_revision = "019eb75ad9ae"
17+
branch_labels = None
18+
depends_on = None
19+
20+
21+
def upgrade():
22+
# ### commands auto generated by Alembic - please adjust! ###
23+
op.create_table(
24+
"score_calibrations",
25+
sa.Column("id", sa.Integer(), nullable=False),
26+
sa.Column("urn", sa.String(length=64), nullable=True),
27+
sa.Column("score_set_id", sa.Integer(), nullable=False),
28+
sa.Column("title", sa.String(), nullable=False),
29+
sa.Column("research_use_only", sa.Boolean(), nullable=False),
30+
sa.Column("primary", sa.Boolean(), nullable=False),
31+
sa.Column("investigator_provided", sa.Boolean(), nullable=False),
32+
sa.Column("private", sa.Boolean(), nullable=False),
33+
sa.Column("notes", sa.String(), nullable=True),
34+
sa.Column("baseline_score", sa.Float(), nullable=True),
35+
sa.Column("baseline_score_description", sa.String(), nullable=True),
36+
sa.Column("functional_ranges", postgresql.JSONB(astext_type=sa.Text(), none_as_null=True), nullable=True),
37+
sa.Column("calibration_metadata", postgresql.JSONB(astext_type=sa.Text(), none_as_null=True), nullable=True),
38+
sa.Column("created_by_id", sa.Integer(), nullable=False),
39+
sa.Column("modified_by_id", sa.Integer(), nullable=False),
40+
sa.Column("creation_date", sa.Date(), nullable=False),
41+
sa.Column("modification_date", sa.Date(), nullable=False),
42+
sa.ForeignKeyConstraint(
43+
["score_set_id"],
44+
["scoresets.id"],
45+
),
46+
sa.ForeignKeyConstraint(
47+
["created_by_id"],
48+
["users.id"],
49+
),
50+
sa.ForeignKeyConstraint(
51+
["modified_by_id"],
52+
["users.id"],
53+
),
54+
sa.PrimaryKeyConstraint("id"),
55+
)
56+
op.create_table(
57+
"score_calibration_publication_identifiers",
58+
sa.Column("score_calibration_id", sa.Integer(), nullable=False),
59+
sa.Column("publication_identifier_id", sa.Integer(), nullable=False),
60+
sa.Column(
61+
"relation",
62+
sa.Enum(
63+
"thresholds",
64+
"classifications",
65+
"methods",
66+
name="scorecalibrationrelation",
67+
native_enum=False,
68+
length=32,
69+
),
70+
nullable=False,
71+
),
72+
sa.ForeignKeyConstraint(
73+
["publication_identifier_id"],
74+
["publication_identifiers.id"],
75+
),
76+
sa.ForeignKeyConstraint(
77+
["score_calibration_id"],
78+
["score_calibrations.id"],
79+
),
80+
sa.PrimaryKeyConstraint("score_calibration_id", "publication_identifier_id", "relation"),
81+
)
82+
op.create_index(op.f("ix_score_calibrations_urn"), "score_calibrations", ["urn"], unique=True)
83+
84+
# ### end Alembic commands ###
85+
86+
87+
def downgrade():
88+
# ### commands auto generated by Alembic - please adjust! ###
89+
op.drop_index(op.f("ix_score_calibrations_urn"), table_name="score_calibrations")
90+
op.drop_table("score_calibration_publication_identifiers")
91+
op.drop_table("score_calibrations")
92+
# ### end Alembic commands ###
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""remove score range property from score sets
2+
3+
Revision ID: f5a72192fafd
4+
Revises: 002f6f9ec7ac
5+
Create Date: 2025-10-08 15:35:49.275162
6+
7+
"""
8+
9+
from alembic import op
10+
import sqlalchemy as sa
11+
from sqlalchemy.dialects import postgresql
12+
13+
# revision identifiers, used by Alembic.
14+
revision = "f5a72192fafd"
15+
down_revision = "002f6f9ec7ac"
16+
branch_labels = None
17+
depends_on = None
18+
19+
20+
def upgrade():
21+
# ### commands auto generated by Alembic - please adjust! ###
22+
op.drop_column("scoresets", "score_ranges")
23+
# ### end Alembic commands ###
24+
25+
26+
def downgrade():
27+
# ### commands auto generated by Alembic - please adjust! ###
28+
op.add_column(
29+
"scoresets",
30+
sa.Column("score_ranges", postgresql.JSONB(astext_type=sa.Text()), autoincrement=False, nullable=True),
31+
)
32+
# ### end Alembic commands ###

docker-compose-dev.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ services:
4848
- redis
4949

5050
dcd-mapping:
51+
build: ../dcd_mapping
5152
image: dcd-mapping:dev
5253
command: bash -c "uvicorn api.server_main:app --host 0.0.0.0 --port 8000 --reload"
5354
depends_on:
@@ -61,6 +62,7 @@ services:
6162
- mavedb-seqrepo-dev:/usr/local/share/seqrepo
6263

6364
cdot-rest:
65+
build: ../cdot_rest
6466
image: cdot-rest:dev
6567
command: bash -c "gunicorn cdot_rest.wsgi:application --bind 0.0.0.0:8000"
6668
env_file:

0 commit comments

Comments
 (0)