Skip to content

Commit 60d4e0d

Browse files
authored
Merge pull request #406 from VariantEffect/release-2025.1.1
Release 2025.1.1
2 parents 3435abd + 7c809fb commit 60d4e0d

38 files changed

+2970
-1827
lines changed

alembic/versions/9702d32bacb3_controlled_keyword.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ def upgrade():
146146
"""INSERT INTO controlled_keywords (key, value, vocabulary, special, description, creation_date, modification_date) VALUES ('Delivery method', 'Adeno-associated virus transduction', NULL, False, 'How the variant library was delivered to the model system for phenotype evaluation.', NOW(), NOW())"""
147147
)
148148
op.execute(
149-
"""INSERT INTO controlled_keywords (key, value, vocabulary, special, description, creation_date, modification_date) VALUES ('Delivery method', 'Tentivirus transduction', NULL, False, 'How the variant library was delivered to the model system for phenotype evaluation.', NOW(), NOW())"""
149+
"""INSERT INTO controlled_keywords (key, value, vocabulary, special, description, creation_date, modification_date) VALUES ('Delivery method', 'Lentivirus transduction', NULL, False, 'How the variant library was delivered to the model system for phenotype evaluation.', NOW(), NOW())"""
150150
)
151151
op.execute(
152152
"""INSERT INTO controlled_keywords (key, value, vocabulary, special, description, creation_date, modification_date) VALUES ('Delivery method', 'Chemical or heat shock transformation', NULL, False, 'How the variant library was delivered to the model system for phenotype evaluation.', NOW(), NOW())"""
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"""materialized view for variant statistics
2+
Revision ID: b85bc7b1bec7
3+
Revises: c404b6719110
4+
Create Date: 2025-03-14 01:53:19.898198
5+
"""
6+
7+
from alembic import op
8+
from alembic_utils.pg_materialized_view import PGMaterializedView
9+
from sqlalchemy.dialects import postgresql
10+
11+
from mavedb.models.published_variant import signature, definition
12+
13+
14+
# revision identifiers, used by Alembic.
15+
revision = "b85bc7b1bec7"
16+
down_revision = "c404b6719110"
17+
branch_labels = None
18+
depends_on = None
19+
20+
21+
def upgrade():
22+
op.create_entity(
23+
PGMaterializedView(
24+
schema="public",
25+
signature=signature,
26+
definition=definition.compile(dialect=postgresql.dialect()).string,
27+
with_data=True,
28+
)
29+
)
30+
op.create_index(
31+
f"idx_{signature}_variant_id",
32+
signature,
33+
["variant_id"],
34+
unique=False,
35+
)
36+
op.create_index(
37+
f"idx_{signature}_variant_urn",
38+
signature,
39+
["variant_urn"],
40+
unique=False,
41+
)
42+
op.create_index(
43+
f"idx_{signature}_score_set_id",
44+
signature,
45+
["score_set_id"],
46+
unique=False,
47+
)
48+
op.create_index(
49+
f"idx_{signature}_score_set_urn",
50+
signature,
51+
["score_set_urn"],
52+
unique=False,
53+
)
54+
op.create_index(
55+
f"idx_{signature}_mapped_variant_id",
56+
signature,
57+
["mapped_variant_id"],
58+
unique=True,
59+
)
60+
61+
62+
def downgrade():
63+
op.drop_index(f"idx_{signature}_variant_id", signature)
64+
op.drop_index(f"idx_{signature}_variant_urn", signature)
65+
op.drop_index(f"idx_{signature}_mapped_variant_id", signature)
66+
op.drop_index(f"idx_{signature}_score_set_id", signature)
67+
op.drop_index(f"idx_{signature}_score_set_urn", signature)
68+
op.drop_entity(
69+
PGMaterializedView(
70+
schema="public",
71+
signature=signature,
72+
definition=definition.compile(dialect=postgresql.dialect()).string,
73+
with_data=True,
74+
)
75+
)

poetry.lock

Lines changed: 1293 additions & 1231 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 4 additions & 3 deletions
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.1.0"
7+
version = "2025.1.1"
88
description = "API for MaveDB, the database of Multiplexed Assays of Variant Effect."
99
license = "AGPL-3.0-only"
1010
readme = "README.md"
@@ -41,6 +41,7 @@ SQLAlchemy = "~2.0.0"
4141

4242
# Optional dependencies for running this application as a server
4343
alembic = { version = "~1.7.6", optional = true }
44+
alembic-utils = { version = "0.8.1", optional = true }
4445
arq = { version = "~0.25.0", optional = true }
4546
authlib = { version = "~1.3.1", optional = true }
4647
boto3 = { version = "~1.34.97", optional = true }
@@ -51,7 +52,7 @@ fastapi = { version = "~0.95.0", optional = true }
5152
hgvs = { version = "~1.5.4", optional = true }
5253
orcid = { version = "~1.0.3", optional = true }
5354
psycopg2 = { version = "~2.9.3", optional = true }
54-
python-jose = { extras = ["cryptography"], version = "~3.3.0", optional = true }
55+
python-jose = { extras = ["cryptography"], version = "~3.4.0", optional = true }
5556
python-multipart = { version = "~0.0.5", optional = true }
5657
requests = { version = "~2.32.0", optional = true }
5758
starlette = { version = "~0.27.0", optional = true }
@@ -85,7 +86,7 @@ SQLAlchemy = { extras = ["mypy"], version = "~2.0.0" }
8586

8687

8788
[tool.poetry.extras]
88-
server = ["alembic", "arq", "authlib", "biocommons", "boto3", "cdot", "cryptography", "fastapi", "hgvs", "orcid", "psycopg2", "python-jose", "python-multipart", "requests", "starlette", "starlette-context", "slack-sdk", "uvicorn", "watchtower"]
89+
server = ["alembic", "alembic-utils", "arq", "authlib", "biocommons", "boto3", "cdot", "cryptography", "fastapi", "hgvs", "orcid", "psycopg2", "python-jose", "python-multipart", "requests", "starlette", "starlette-context", "slack-sdk", "uvicorn", "watchtower"]
8990

9091

9192
[tool.mypy]

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.1.0"
9+
__version__ = "2025.1.1"
1010

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

src/mavedb/db/view.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
"""
2+
Utilities for managing views via SQLAlchemy.
3+
"""
4+
5+
from functools import partial
6+
7+
import sqlalchemy as sa
8+
from sqlalchemy.ext import compiler
9+
from sqlalchemy.schema import DDLElement, MetaData
10+
from sqlalchemy.orm import Session
11+
12+
from mavedb.db.base import Base
13+
14+
# See: https://github.com/sqlalchemy/sqlalchemy/wiki/Views, https://github.com/jeffwidman/sqlalchemy-postgresql-materialized-views?tab=readme-ov-file
15+
16+
17+
class CreateView(DDLElement):
18+
def __init__(self, name: str, selectable: sa.Select, materialized: bool):
19+
self.name = name
20+
self.selectable = selectable
21+
self.materialized = materialized
22+
23+
24+
class DropView(DDLElement):
25+
def __init__(self, name: str, materialized: bool):
26+
self.name = name
27+
self.materialized = materialized
28+
29+
30+
class MaterializedView(Base):
31+
__abstract__ = True
32+
33+
@classmethod
34+
def refresh(cls, connection, concurrently=True):
35+
"""Refresh this materialized view."""
36+
refresh_mat_view(connection, cls.__table__.fullname, concurrently)
37+
38+
39+
@compiler.compiles(CreateView)
40+
def _create_view(element: CreateView, compiler, **kw):
41+
return "CREATE %s %s AS %s" % (
42+
"MATERIALIZED VIEW" if element.materialized else "VIEW",
43+
element.name,
44+
compiler.sql_compiler.process(element.selectable, literal_binds=True),
45+
)
46+
47+
48+
@compiler.compiles(DropView)
49+
def _drop_view(element: DropView, compiler, **kw):
50+
return "DROP %s %s" % ("MATERIALIZED VIEW" if element.materialized else "VIEW", element.name)
51+
52+
53+
def view_exists(ddl: CreateView, target, connection: sa.Connection, materialized: bool, **kw):
54+
inspector = sa.inspect(connection)
55+
if inspector is None:
56+
return False
57+
58+
view_names = inspector.get_materialized_view_names() if ddl.materialized else inspector.get_view_names()
59+
return ddl.name in view_names
60+
61+
62+
def view_doesnt_exist(ddl: CreateView, target, connection: sa.Connection, materialized: bool, **kw):
63+
return not view_exists(ddl, target, connection, materialized, **kw)
64+
65+
66+
def view(name: str, selectable: sa.Select, metadata: MetaData = Base.metadata, materialized: bool = False):
67+
"""
68+
Register a view or materialized view to SQLAlchemy. Use this function to define a view on some arbitrary
69+
model class.
70+
71+
```
72+
class MyView(Base):
73+
__table__ = view(
74+
"my_view",
75+
select(
76+
MyModel.id.label("id"),
77+
MyModel.name.label("name"),
78+
),
79+
materialized=False,
80+
)
81+
```
82+
83+
When registered in this manner, SQLAlchemy will create and destroy the view along with other tables. You can
84+
then query this view as if it were an ORM object.
85+
86+
```
87+
results = db.query(select(MyView.col1).where(MyView.col2)).all()
88+
```
89+
"""
90+
t = sa.table(
91+
name,
92+
*(sa.Column(c.name, c.type, primary_key=c.primary_key) for c in selectable.selected_columns),
93+
)
94+
t.primary_key.update(c for c in t.c if c.primary_key) # type: ignore
95+
96+
# TODO: Figure out indices.
97+
if materialized:
98+
sa.event.listen(
99+
metadata,
100+
"after_create",
101+
CreateView(name, selectable, True).execute_if(callable_=partial(view_doesnt_exist, materialized=True)),
102+
)
103+
sa.event.listen(
104+
metadata,
105+
"before_drop",
106+
DropView(name, True).execute_if(callable_=partial(view_exists, materialized=True)),
107+
)
108+
109+
else:
110+
sa.event.listen(
111+
metadata,
112+
"after_create",
113+
CreateView(name, selectable, False).execute_if(callable_=partial(view_doesnt_exist, materialized=False)),
114+
)
115+
sa.event.listen(
116+
metadata,
117+
"before_drop",
118+
DropView(name, False).execute_if(callable_=partial(view_exists, materialized=False)),
119+
)
120+
121+
return t
122+
123+
124+
def refresh_mat_view(session: Session, name: str, concurrently=True):
125+
"""
126+
Refreshes a single materialized view, given by `name`.
127+
"""
128+
# since session.execute() bypasses autoflush, must manually flush in order
129+
# to include newly-created/modified objects in the refresh
130+
session.flush()
131+
_con = "CONCURRENTLY " if concurrently else ""
132+
session.execute(sa.text("REFRESH MATERIALIZED VIEW " + _con + name))
133+
134+
135+
def refresh_all_mat_views(session: Session, concurrently=True):
136+
"""
137+
Refreshes all materialized views. Views are refreshed in non-deterministic order,
138+
so view definitions can't depend on each other.
139+
"""
140+
inspector = sa.inspect(session.connection())
141+
142+
if not inspector:
143+
return
144+
145+
for mv in inspector.get_materialized_view_names():
146+
refresh_mat_view(session, mv, concurrently)

src/mavedb/models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"license",
1313
"mapped_variant",
1414
"publication_identifier",
15+
"published_variant",
1516
"raw_read_identifier",
1617
"refseq_identifier",
1718
"refseq_offset",
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from sqlalchemy import select, join
2+
3+
from mavedb.db.view import MaterializedView, view
4+
5+
from mavedb.models.score_set import ScoreSet
6+
from mavedb.models.variant import Variant
7+
from mavedb.models.mapped_variant import MappedVariant
8+
9+
10+
signature = "published_variants_materialized_view"
11+
definition = (
12+
select(
13+
Variant.id.label("variant_id"),
14+
Variant.urn.label("variant_urn"),
15+
MappedVariant.id.label("mapped_variant_id"),
16+
ScoreSet.id.label("score_set_id"),
17+
ScoreSet.urn.label("score_set_urn"),
18+
ScoreSet.published_date.label("published_date"),
19+
MappedVariant.current.label("current_mapped_variant"),
20+
)
21+
.select_from(
22+
join(Variant, MappedVariant, Variant.id == MappedVariant.variant_id, isouter=True).join(
23+
ScoreSet, ScoreSet.id == Variant.score_set_id
24+
)
25+
)
26+
.where(
27+
ScoreSet.published_date.is_not(None),
28+
)
29+
)
30+
31+
32+
class PublishedVariantsMV(MaterializedView):
33+
__table__ = view(
34+
signature,
35+
definition,
36+
materialized=True,
37+
)
38+
39+
variant_id = __table__.c.variant_id
40+
variant_urn = __table__.c.variant_urn
41+
mapped_variant_id = __table__.c.mapped_variant_id
42+
score_set_id = __table__.c.score_set_id
43+
score_set_urn = __table__.c.score_set_urn
44+
published_date = __table__.c.published_date
45+
current_mapped_variant = __table__.c.current_mapped_variant

0 commit comments

Comments
 (0)