Skip to content

Commit 1f3af16

Browse files
committed
Solved the merge conflicts and modified the related tests.
2 parents 7d0fc6e + b155ed6 commit 1f3af16

Some content is hidden

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

55 files changed

+3112
-896
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

alembic/manual_migrations/migrate_consolidated_score_ranges_and_calibrations.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from sqlalchemy.orm import Session
1010

1111
from mavedb.models.score_set import ScoreSet
12-
from mavedb.view_models.score_range import ScoreSetRangesCreate, InvestigatorScoreRangesCreate, PillarProjectScoreRangesCreate, PillarProjectScoreRangeCreate
12+
from mavedb.view_models.score_range import ScoreSetRangesCreate, InvestigatorScoreRangesCreate, ZeibergCalibrationScoreRangesCreate, ZeibergCalibrationScoreRangeCreate
1313

1414

1515
from mavedb.db.session import SessionLocal
@@ -47,11 +47,11 @@ def do_migration(db: Session):
4747
investigator_ranges = None
4848

4949
if score_set.score_calibrations is not None:
50-
thresholds = score_set.score_calibrations.get("pillar_project", {}).get("thresholds", [])
51-
evidence_strengths = score_set.score_calibrations.get("pillar_project", {}).get("evidence_strengths", [])
52-
positive_likelihood_ratios = score_set.score_calibrations.get("pillar_project", {}).get("positive_likelihood_ratios", [])
53-
prior_probability_pathogenicity = score_set.score_calibrations.get("pillar_project", {}).get("prior_probability_pathogenicity", None)
54-
parameter_sets = score_set.score_calibrations.get("pillar_project", {}).get("parameter_sets", [])
50+
thresholds = score_set.score_calibrations.get("zeiberg_calibration", {}).get("thresholds", [])
51+
evidence_strengths = score_set.score_calibrations.get("zeiberg_calibration", {}).get("evidence_strengths", [])
52+
positive_likelihood_ratios = score_set.score_calibrations.get("zeiberg_calibration", {}).get("positive_likelihood_ratios", [])
53+
prior_probability_pathogenicity = score_set.score_calibrations.get("zeiberg_calibration", {}).get("prior_probability_pathogenicity", None)
54+
parameter_sets = score_set.score_calibrations.get("zeiberg_calibration", {}).get("parameter_sets", [])
5555

5656
ranges = []
5757
boundary_direction = -1 # Start with a negative sign to indicate the first range has the lower boundary appearing prior to the threshold
@@ -60,7 +60,7 @@ def do_migration(db: Session):
6060

6161
if idx == 0:
6262
calculated_range = (None, threshold)
63-
ranges.append(PillarProjectScoreRangeCreate(
63+
ranges.append(ZeibergCalibrationScoreRangeCreate(
6464
range=(None, threshold),
6565
classification="normal" if evidence_strength < 0 else "abnormal",
6666
label=str(evidence_strength),
@@ -71,7 +71,7 @@ def do_migration(db: Session):
7171
))
7272
elif idx == len(thresholds) - 1:
7373
calculated_range = (threshold, None)
74-
ranges.append(PillarProjectScoreRangeCreate(
74+
ranges.append(ZeibergCalibrationScoreRangeCreate(
7575
range=(threshold, None),
7676
classification="normal" if evidence_strength < 0 else "abnormal",
7777
evidence_strength=evidence_strength,
@@ -86,7 +86,7 @@ def do_migration(db: Session):
8686
else:
8787
calculated_range = (threshold, thresholds[idx + 1])
8888

89-
ranges.append(PillarProjectScoreRangeCreate(
89+
ranges.append(ZeibergCalibrationScoreRangeCreate(
9090
range=calculated_range,
9191
classification="normal" if evidence_strength < 0 else "abnormal",
9292
label=str(evidence_strength),
@@ -100,17 +100,17 @@ def do_migration(db: Session):
100100
if idx != len(evidence_strengths) - 1 and (evidence_strengths[idx + 1] * evidence_strength < 0):
101101
boundary_direction = -boundary_direction
102102

103-
pillar_project_ranges = PillarProjectScoreRangesCreate(
103+
zeiberg_calibration_ranges = ZeibergCalibrationScoreRangesCreate(
104104
prior_probability_pathogenicity=prior_probability_pathogenicity,
105105
parameter_sets=parameter_sets,
106106
ranges=ranges,
107107
)
108108
else:
109-
pillar_project_ranges = None
109+
zeiberg_calibration_ranges = None
110110

111111
score_set.score_ranges = ScoreSetRangesCreate(
112112
investigator_provided=investigator_ranges if investigator_ranges else None,
113-
pillar_project=pillar_project_ranges if pillar_project_ranges else None,
113+
zeiberg_calibration=zeiberg_calibration_ranges if zeiberg_calibration_ranges else None,
114114
).model_dump()
115115
db.add(score_set)
116116

docker-compose-dev.yml

Lines changed: 5 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,14 +62,18 @@ 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:
6769
- settings/.env.dev
6870
depends_on:
6971
- redis
72+
- seqrepo
7073
ports:
7174
- "8006:8000"
75+
volumes:
76+
- mavedb-seqrepo-dev:/usr/local/share/seqrepo
7277

7378
db:
7479
image: postgres:14

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
44

55
[tool.poetry]
66
name = "mavedb"
7-
version = "2025.3.0"
7+
version = "2025.4.1"
88
description = "API for MaveDB, the database of Multiplexed Assays of Variant Effect."
99
license = "AGPL-3.0-only"
1010
readme = "README.md"

settings/.env.template

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,10 @@ DCD_MAPPING_URL=http://dcd-mapping:8000
6767
####################################################################################################
6868

6969
CDOT_URL=http://cdot-rest:8000
70-
REDIS_HOST=localhost
70+
REDIS_HOST=redis
71+
REDIS_IP=redis
7172
REDIS_PORT=6379
73+
REDIS_SSL=false
7274

7375
####################################################################################################
7476
# Environment variables for ClinGen

src/mavedb/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@
66
logger = module_logging.getLogger(__name__)
77

88
__project__ = "mavedb-api"
9-
__version__ = "2025.3.0"
9+
__version__ = "2025.4.1"
1010

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

src/mavedb/db/view.py

Lines changed: 123 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66

77
import sqlalchemy as sa
88
from sqlalchemy.ext import compiler
9-
from sqlalchemy.schema import DDLElement, MetaData
109
from sqlalchemy.orm import Session
10+
from sqlalchemy.schema import DDLElement, MetaData
1111

1212
from mavedb.db.base import Base
1313

@@ -32,7 +32,53 @@ class MaterializedView(Base):
3232

3333
@classmethod
3434
def refresh(cls, connection, concurrently=True):
35-
"""Refresh this materialized view."""
35+
"""
36+
Refresh the underlying materialized view for this ORM-mapped class.
37+
38+
This class method delegates to `refresh_mat_view` to issue a database
39+
REFRESH MATERIALIZED VIEW (optionally CONCURRENTLY) statement for the
40+
materialized view backing the current model (`cls.__table__.fullname`).
41+
42+
Parameters
43+
---------
44+
connection : sqlalchemy.engine.Connection | sqlalchemy.orm.Session
45+
An active SQLAlchemy connection or session bound to the target database.
46+
concurrently : bool, default True
47+
If True, performs a concurrent refresh (REFRESH MATERIALIZED VIEW CONCURRENTLY),
48+
allowing reads during the refresh when the database backend supports it.
49+
If False, performs a blocking refresh.
50+
51+
Returns
52+
-------
53+
None
54+
55+
Raises
56+
------
57+
sqlalchemy.exc.DBAPIError
58+
If the database reports an error while refreshing the materialized view.
59+
sqlalchemy.exc.OperationalError
60+
For operational issues such as locks or insufficient privileges.
61+
ValueError
62+
If the connection provided is not a valid SQLAlchemy connection/session.
63+
64+
Notes
65+
-----
66+
- A concurrent refresh typically requires the materialized view to have a unique
67+
index matching all rows; otherwise the database may reject the operation.
68+
- This operation does not return a value; it is executed for its side effect.
69+
- Ensure the connection/session is in a clean transactional state if you rely on
70+
consistent snapshot semantics.
71+
- This function commits no changes; it is the caller's responsibility to
72+
commit the session if needed.
73+
74+
Examples
75+
--------
76+
# Refresh with concurrent mode (default)
77+
MyMaterializedView.refresh(connection)
78+
79+
# Perform a blocking refresh
80+
MyMaterializedView.refresh(connection, concurrently=False)
81+
"""
3682
refresh_mat_view(connection, cls.__table__.fullname, concurrently)
3783

3884

@@ -123,19 +169,91 @@ class MyView(Base):
123169

124170
def refresh_mat_view(session: Session, name: str, concurrently=True):
125171
"""
126-
Refreshes a single materialized view, given by `name`.
172+
Refresh a PostgreSQL materialized view within the current SQLAlchemy session.
173+
174+
This helper issues a REFRESH MATERIALIZED VIEW statement for the specified
175+
materialized view name. It first explicitly flushes the session because
176+
session.execute() bypasses SQLAlchemy's autoflush mechanism; without the flush,
177+
pending changes (e.g., newly inserted/updated rows that the view depends on)
178+
might not be reflected in the refreshed view.
179+
180+
Parameters
181+
----------
182+
session : sqlalchemy.orm.Session
183+
An active SQLAlchemy session bound to a PostgreSQL database.
184+
name : str
185+
The exact name (optionally schema-qualified) of the materialized view to refresh.
186+
concurrently : bool, default True
187+
If True, uses REFRESH MATERIALIZED VIEW CONCURRENTLY allowing reads during
188+
the refresh and requiring a unique index on the materialized view. If False,
189+
performs a blocking refresh.
190+
191+
Raises
192+
------
193+
sqlalchemy.exc.SQLAlchemyError
194+
Propagates any database errors encountered during execution (e.g.,
195+
insufficient privileges, missing view, lack of required unique index for
196+
CONCURRENTLY).
197+
198+
Notes
199+
-----
200+
- Using CONCURRENTLY requires the materialized view to have at least one
201+
unique index; otherwise PostgreSQL will raise an error.
202+
- The operation does not return a value; it is executed for its side effect.
203+
- Ensure the session is in a clean transactional state if you rely on
204+
consistent snapshot semantics.
205+
- This function commits no changes; it is the caller's responsibility to
206+
commit the session if needed.
207+
208+
Examples
209+
--------
210+
refresh_mat_view(session, "public.my_materialized_view")
211+
refresh_mat_view(session, "reports.daily_stats", concurrently=False)
127212
"""
128213
# since session.execute() bypasses autoflush, must manually flush in order
129214
# to include newly-created/modified objects in the refresh
130215
session.flush()
216+
131217
_con = "CONCURRENTLY " if concurrently else ""
132218
session.execute(sa.text("REFRESH MATERIALIZED VIEW " + _con + name))
133219

134220

135221
def refresh_all_mat_views(session: Session, concurrently=True):
136222
"""
137-
Refreshes all materialized views. Views are refreshed in non-deterministic order,
138-
so view definitions can't depend on each other.
223+
Refreshes all PostgreSQL materialized views visible to the given SQLAlchemy session.
224+
225+
The function inspects the current database connection for registered materialized
226+
views and issues a REFRESH MATERIALIZED VIEW command for each one using the helper
227+
function `refresh_mat_view`. After all refresh operations complete, the session
228+
is committed to persist any transactional side effects of the refresh statements.
229+
230+
Parameters
231+
----------
232+
session : sqlalchemy.orm.Session
233+
An active SQLAlchemy session bound to a PostgreSQL connection.
234+
concurrently : bool, default True
235+
If True, each materialized view is refreshed using the CONCURRENTLY option
236+
(only supported when the view has a unique index that satisfies PostgreSQL
237+
requirements). If False, a standard blocking refresh is performed.
238+
239+
Behavior
240+
--------
241+
- If inspection of the connection fails or returns no inspector, the function
242+
exits without performing any work.
243+
- Each materialized view name returned by the inspector is passed to
244+
`refresh_mat_view(session, name, concurrently)`.
245+
246+
Notes
247+
-----
248+
- Using CONCURRENTLY allows reads during refresh at the cost of requiring an
249+
appropriate unique index and potentially being slower.
250+
- Exceptions raised during individual refresh operations will propagate unless
251+
`refresh_mat_view` handles them internally; in such a case the commit will
252+
not be reached.
253+
- Ensure the session is in a clean transactional state if you rely on
254+
consistent snapshot semantics.
255+
- This function commits no changes; it is the caller's responsibility to
256+
commit the session if needed.
139257
"""
140258
inspector = sa.inspect(session.connection())
141259

src/mavedb/lib/annotation/agent.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,14 @@ def mavedb_user_agent(user: User) -> Agent:
5858

5959

6060
# XXX: Ideally, this becomes versioned software.
61-
def pillar_project_calibration_agent() -> Agent:
61+
def zeiberg_calibration_agent() -> Agent:
6262
"""
6363
Create a [VA Agent](https://va-ga4gh.readthedocs.io/en/latest/core-information-model/entities/agent.html)
64-
object for the pillar project calibration software.
64+
object for the Zeiberg calibration software.
6565
"""
6666
return Agent(
67-
name="Pillar Project Variant Calibrator",
67+
name="Zeiberg Variant Calibrator",
6868
agentType="Software",
6969
# XXX - version?
70-
description="Pillar project variant calibrator, see https://github.com/Dzeiberg/mave_calibration",
70+
description="Zeiberg variant calibrator, see https://github.com/Dzeiberg/mave_calibration",
7171
)

src/mavedb/lib/annotation/classification.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from ga4gh.va_spec.base.enums import StrengthOfEvidenceProvided
77

88
from mavedb.models.mapped_variant import MappedVariant
9-
from mavedb.lib.annotation.constants import PILLAR_PROJECT_CALIBRATION_STRENGTH_OF_EVIDENCE_MAP
9+
from mavedb.lib.annotation.constants import ZEIBERG_CALIBRATION_CALIBRATION_STRENGTH_OF_EVIDENCE_MAP
1010
from mavedb.lib.validation.utilities import inf_or_float
1111
from mavedb.view_models.score_range import ScoreSetRanges
1212

@@ -33,7 +33,7 @@ def functional_classification_of_variant(
3333
# This view model object is much simpler to work with.
3434
score_ranges = ScoreSetRanges(**mapped_variant.variant.score_set.score_ranges).investigator_provided
3535

36-
if not score_ranges:
36+
if not score_ranges or not score_ranges.ranges:
3737
raise ValueError(
3838
f"Variant {mapped_variant.variant.urn} does not have investigator-provided score ranges."
3939
" Unable to classify functional impact."
@@ -60,7 +60,7 @@ def functional_classification_of_variant(
6060
return ExperimentalVariantFunctionalImpactClassification.INDETERMINATE
6161

6262

63-
def pillar_project_clinical_classification_of_variant(
63+
def zeiberg_calibration_clinical_classification_of_variant(
6464
mapped_variant: MappedVariant,
6565
) -> tuple[VariantPathogenicityEvidenceLine.Criterion, Optional[StrengthOfEvidenceProvided]]:
6666
if mapped_variant.variant.score_set.score_ranges is None:
@@ -69,9 +69,9 @@ def pillar_project_clinical_classification_of_variant(
6969
" Unable to classify clinical impact."
7070
)
7171

72-
score_ranges = ScoreSetRanges(**mapped_variant.variant.score_set.score_ranges).pillar_project
72+
score_ranges = ScoreSetRanges(**mapped_variant.variant.score_set.score_ranges).zeiberg_calibration
7373

74-
if not score_ranges:
74+
if not score_ranges or not score_ranges.ranges:
7575
raise ValueError(
7676
f"Variant {mapped_variant.variant.urn} does not have pillar project score ranges."
7777
" Unable to classify clinical impact."
@@ -88,6 +88,6 @@ def pillar_project_clinical_classification_of_variant(
8888
for range in score_ranges.ranges:
8989
lower_bound, upper_bound = inf_or_float(range.range[0], lower=True), inf_or_float(range.range[1], lower=False)
9090
if functional_score > lower_bound and functional_score <= upper_bound:
91-
return PILLAR_PROJECT_CALIBRATION_STRENGTH_OF_EVIDENCE_MAP[range.evidence_strength]
91+
return ZEIBERG_CALIBRATION_CALIBRATION_STRENGTH_OF_EVIDENCE_MAP[range.evidence_strength]
9292

93-
return PILLAR_PROJECT_CALIBRATION_STRENGTH_OF_EVIDENCE_MAP[0]
93+
return ZEIBERG_CALIBRATION_CALIBRATION_STRENGTH_OF_EVIDENCE_MAP[0]

src/mavedb/lib/annotation/constants.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
GENERIC_DISEASE_MEDGEN_CODE = "C0012634"
55
MEDGEN_SYSTEM = "https://www.ncbi.nlm.nih.gov/medgen/"
66

7-
PILLAR_PROJECT_CALIBRATION_STRENGTH_OF_EVIDENCE_MAP = {
7+
ZEIBERG_CALIBRATION_CALIBRATION_STRENGTH_OF_EVIDENCE_MAP = {
88
# No evidence
99
0: (VariantPathogenicityEvidenceLine.Criterion.PS3, None),
1010
# Supporting evidence
@@ -31,4 +31,4 @@
3131

3232
# TODO#493
3333
FUNCTIONAL_RANGES = ["investigator_provided"]
34-
CLINICAL_RANGES = ["pillar_project"]
34+
CLINICAL_RANGES = ["zeiberg_calibration"]

0 commit comments

Comments
 (0)