omop-constructs 0.4.0__tar.gz → 0.4.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/PKG-INFO +2 -2
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/pyproject.toml +2 -2
- omop_constructs-0.4.2/src/omop_constructs/alchemy/__init__.py +7 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/episodes/__init__.py +3 -1
- omop_constructs-0.4.2/src/omop_constructs/alchemy/episodes/consult_window_mv.py +57 -0
- omop_constructs-0.4.2/src/omop_constructs/alchemy/episodes/consult_window_query.py +261 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/episodes/treatment_summary_mv.py +2 -1
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/events/__init__.py +5 -1
- omop_constructs-0.4.2/src/omop_constructs/alchemy/events/dx_linked_visit_mv.py +56 -0
- omop_constructs-0.4.2/src/omop_constructs/alchemy/events/visit_queries.py +89 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/modifiers/modifier_joins.py +9 -7
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/core/constructs.py +14 -2
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/core/materialized.py +12 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/core/plan.py +9 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/core/registry.py +25 -1
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/semantics/lookups.py +10 -1
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/semantics/runtime_lookups.py +10 -1
- omop_constructs-0.4.2/src/omop_constructs/services/__init__.py +5 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/typing.py +10 -1
- omop_constructs-0.4.0/src/omop_constructs/alchemy/__init__.py +0 -0
- omop_constructs-0.4.0/src/omop_constructs/services/__init__.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/README.md +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/__init__.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/adapters.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/concepts/__init__.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/concepts/concept_mappers.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/concepts/concept_standardisation.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/conditions/__init__.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/conditions/condition_joins.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/conditions/condition_mappers.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/demography/__init__.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/demography/demography_matview.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/demography/demography_queries.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/episodes/condition_episode_mv.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/episodes/condition_episode_objects.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/episodes/course_joins.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/episodes/course_mv.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/episodes/cycle_join.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/episodes/cycle_mv.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/episodes/episode_factories.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/episodes/episode_joins.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/episodes/fraction_joins.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/episodes/fraction_mv.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/episodes/modality_intent_join.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/episodes/modality_intent_mv.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/episodes/surgical_joins.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/episodes/surgical_procedure_mv.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/episodes/systemic_treatment_joins.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/episodes/systemic_treatment_mv.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/episodes/treatment_envelope_mv.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/episodes/treatment_envelope_query.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/episodes/treatment_summary_joins.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/events/dx_linked_event_mv.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/events/dx_linked_obs_mv.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/events/dx_linked_procedure_mv.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/events/event_factories.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/events/event_queries.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/events/observation_queries.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/events/procedure_queries.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/modifiers/__init__.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/modifiers/condition_modifier_join.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/modifiers/condition_modifier_mv.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/modifiers/modifier_factories.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/modifiers/modifier_mappers.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/modifiers/procedure_modifier_joins.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/modifiers/procedure_modifier_mv.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/source_factories.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/core/__init__.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/core/base.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/core/ddl.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/core/errors.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/py.typed +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/semantics/__init__.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/semantics/adapters.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/services/concept.py +0 -0
- {omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/services/person.py +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: omop-constructs
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: For building complex constructs on top of the omop-alchemy library.
|
|
5
5
|
Author: Georgie Kennedy
|
|
6
6
|
Author-email: Georgie Kennedy <georgina.kennedy@unsw.edu.au>
|
|
7
7
|
Requires-Dist: omop-alchemy>=0.6.1
|
|
8
|
-
Requires-Dist: omop-semantics>=0.1.
|
|
8
|
+
Requires-Dist: omop-semantics>=0.1.15
|
|
9
9
|
Requires-Dist: psycopg2-binary>=2.9.11
|
|
10
10
|
Requires-Dist: sqlalchemy>=2.0.45
|
|
11
11
|
Requires-Dist: ipython>=9.8.0 ; extra == 'dev'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "omop-constructs"
|
|
3
|
-
version = "0.4.
|
|
3
|
+
version = "0.4.2"
|
|
4
4
|
description = "For building complex constructs on top of the omop-alchemy library."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -9,7 +9,7 @@ authors = [
|
|
|
9
9
|
requires-python = ">=3.12"
|
|
10
10
|
dependencies = [
|
|
11
11
|
"omop-alchemy>=0.6.1",
|
|
12
|
-
"omop-semantics>=0.1.
|
|
12
|
+
"omop-semantics>=0.1.15",
|
|
13
13
|
"psycopg2-binary>=2.9.11",
|
|
14
14
|
"sqlalchemy>=2.0.45",
|
|
15
15
|
]
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""
|
|
2
|
+
High-level construct families built on top of ``omop-alchemy`` models.
|
|
3
|
+
|
|
4
|
+
The subpackages under :mod:`omop_constructs.alchemy` expose the repository's
|
|
5
|
+
active analytical constructs, mostly as ORM-mapped materialized views plus the
|
|
6
|
+
query fragments used to build them.
|
|
7
|
+
"""
|
{omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/episodes/__init__.py
RENAMED
|
@@ -8,6 +8,7 @@ from .fraction_mv import FractionMV
|
|
|
8
8
|
from .treatment_summary_mv import ConditionTreatmentEpisode
|
|
9
9
|
from .treatment_envelope_mv import TreatmentEnvelopeMV
|
|
10
10
|
from .modality_intent_mv import TreatmentIntentMV, ConditionTreatmentIntentMV
|
|
11
|
+
from .consult_window_mv import ConsultWindowMV
|
|
11
12
|
|
|
12
13
|
__all__ = [
|
|
13
14
|
"OverarchingDiseaseEpisodeMV",
|
|
@@ -24,4 +25,5 @@ __all__ = [
|
|
|
24
25
|
"TreatmentEnvelopeMV",
|
|
25
26
|
"TreatmentIntentMV",
|
|
26
27
|
"ConditionTreatmentIntentMV",
|
|
27
|
-
|
|
28
|
+
"ConsultWindowMV",
|
|
29
|
+
]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import sqlalchemy as sa
|
|
2
|
+
import sqlalchemy.orm as so
|
|
3
|
+
from datetime import date
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from orm_loader.helpers import Base
|
|
7
|
+
|
|
8
|
+
from ...core.constructs import register_construct
|
|
9
|
+
from ...core.materialized import MaterializedViewMixin
|
|
10
|
+
from ..events.dx_linked_obs_mv import DxObservationMV
|
|
11
|
+
from ..events.dx_linked_visit_mv import DxRelevantVisitMV
|
|
12
|
+
from .consult_window_query import consult_window
|
|
13
|
+
from .treatment_envelope_mv import TreatmentEnvelopeMV
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@register_construct
|
|
17
|
+
class ConsultWindowMV(MaterializedViewMixin, Base):
|
|
18
|
+
"""
|
|
19
|
+
Episode-of-care consult and referral window summary.
|
|
20
|
+
|
|
21
|
+
The materialized view exposes the earliest specialist contact and the
|
|
22
|
+
derived scalar windows currently used downstream for referral timing
|
|
23
|
+
analysis.
|
|
24
|
+
"""
|
|
25
|
+
__mv_name__ = "consult_window_mv"
|
|
26
|
+
__mv_select__ = consult_window.select()
|
|
27
|
+
__mv_index__ = "episode_id"
|
|
28
|
+
__deps__ = (
|
|
29
|
+
DxObservationMV.__mv_name__,
|
|
30
|
+
DxRelevantVisitMV.__mv_name__,
|
|
31
|
+
TreatmentEnvelopeMV.__mv_name__,
|
|
32
|
+
)
|
|
33
|
+
__tablename__ = __mv_name__
|
|
34
|
+
__table_args__ = {"extend_existing": True}
|
|
35
|
+
|
|
36
|
+
mv_id: so.Mapped[int] = so.mapped_column(primary_key=True)
|
|
37
|
+
person_id: so.Mapped[int] = so.mapped_column(sa.Integer)
|
|
38
|
+
episode_id: so.Mapped[int] = so.mapped_column(sa.Integer)
|
|
39
|
+
episode_start_date: so.Mapped[date] = so.mapped_column(sa.Date)
|
|
40
|
+
episode_concept_id: so.Mapped[int] = so.mapped_column(sa.Integer)
|
|
41
|
+
initial_gp_referral: so.Mapped[Optional[date]] = so.mapped_column(
|
|
42
|
+
sa.Date, nullable=True
|
|
43
|
+
)
|
|
44
|
+
first_specialist: so.Mapped[Optional[date]] = so.mapped_column(sa.Date, nullable=True)
|
|
45
|
+
first_pall_care: so.Mapped[Optional[date]] = so.mapped_column(sa.Date, nullable=True)
|
|
46
|
+
first_pall_care_or_treatment: so.Mapped[Optional[date]] = so.mapped_column(
|
|
47
|
+
sa.Date, nullable=True
|
|
48
|
+
)
|
|
49
|
+
earliest_treatment: so.Mapped[Optional[date]] = so.mapped_column(
|
|
50
|
+
sa.Date, nullable=True
|
|
51
|
+
)
|
|
52
|
+
referral_to_specialist: so.Mapped[Optional[float]] = so.mapped_column(
|
|
53
|
+
sa.Float, nullable=True
|
|
54
|
+
)
|
|
55
|
+
referral_to_tx: so.Mapped[Optional[float]] = so.mapped_column(
|
|
56
|
+
sa.Float, nullable=True
|
|
57
|
+
)
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Episode-level consult and referral window query fragments.
|
|
3
|
+
|
|
4
|
+
The current implementation combines episode-of-care anchors, diagnosis-linked
|
|
5
|
+
consult observations, ranked specialist visits, and the treatment envelope to
|
|
6
|
+
derive referral-to-specialist and referral-to-treatment windows.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import sqlalchemy as sa
|
|
10
|
+
from sqlalchemy.sql import ColumnElement
|
|
11
|
+
|
|
12
|
+
from omop_alchemy.cdm.model import Episode
|
|
13
|
+
from omop_semantics.runtime.default_valuesets import runtime # type: ignore
|
|
14
|
+
|
|
15
|
+
from ..events.dx_linked_obs_mv import DxObservationMV
|
|
16
|
+
from ..events.dx_linked_visit_mv import DxRelevantVisitMV
|
|
17
|
+
from .treatment_envelope_mv import TreatmentEnvelopeMV
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def days_between(
|
|
21
|
+
end_date: ColumnElement,
|
|
22
|
+
start_date: ColumnElement,
|
|
23
|
+
) -> ColumnElement:
|
|
24
|
+
"""
|
|
25
|
+
Return the interval between two date-like expressions in days.
|
|
26
|
+
"""
|
|
27
|
+
return sa.func.extract(
|
|
28
|
+
"epoch",
|
|
29
|
+
sa.cast(end_date, sa.DateTime) - sa.cast(start_date, sa.DateTime),
|
|
30
|
+
) / 86400.0
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
episode_of_care = (
|
|
34
|
+
sa.select(
|
|
35
|
+
Episode.person_id,
|
|
36
|
+
Episode.episode_id,
|
|
37
|
+
Episode.episode_start_date,
|
|
38
|
+
Episode.episode_concept_id,
|
|
39
|
+
)
|
|
40
|
+
.where(
|
|
41
|
+
Episode.episode_concept_id
|
|
42
|
+
== runtime.types.disease_episode_types.episode_of_care # type: ignore[attr-defined]
|
|
43
|
+
)
|
|
44
|
+
.subquery(name="episode_of_care")
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
episode_of_care_obs = (
|
|
48
|
+
sa.select(*DxObservationMV.__table__.c)
|
|
49
|
+
.where(
|
|
50
|
+
DxObservationMV.episode_concept_id
|
|
51
|
+
== runtime.types.disease_episode_types.episode_of_care # type: ignore[attr-defined]
|
|
52
|
+
)
|
|
53
|
+
.subquery(name="episode_of_care_obs")
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
first_specialist = (
|
|
57
|
+
sa.select(
|
|
58
|
+
episode_of_care_obs.c.person_id,
|
|
59
|
+
episode_of_care_obs.c.episode_id,
|
|
60
|
+
sa.func.min(episode_of_care_obs.c.event_date).label("first_specialist_consult"),
|
|
61
|
+
sa.func.max(episode_of_care_obs.c.event_date).label("last_specialist_consult"),
|
|
62
|
+
)
|
|
63
|
+
.where(
|
|
64
|
+
episode_of_care_obs.c.event_concept_id.in_(
|
|
65
|
+
[
|
|
66
|
+
runtime.cancer_procedures.cancer_consult_types.medonc, # type: ignore[attr-defined]
|
|
67
|
+
runtime.cancer_procedures.cancer_consult_types.clinonc, # type: ignore[attr-defined]
|
|
68
|
+
]
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
.group_by(
|
|
72
|
+
episode_of_care_obs.c.person_id,
|
|
73
|
+
episode_of_care_obs.c.episode_id,
|
|
74
|
+
)
|
|
75
|
+
.subquery(name="first_specialist")
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
gp_referral = (
|
|
79
|
+
sa.select(
|
|
80
|
+
episode_of_care_obs.c.person_id,
|
|
81
|
+
episode_of_care_obs.c.episode_id,
|
|
82
|
+
sa.func.min(episode_of_care_obs.c.event_date).label("initial_gp_referral"),
|
|
83
|
+
)
|
|
84
|
+
.where(
|
|
85
|
+
episode_of_care_obs.c.event_concept_id
|
|
86
|
+
== runtime.cancer_procedures.cancer_consult_types.oncology_referral # type: ignore[attr-defined]
|
|
87
|
+
)
|
|
88
|
+
.group_by(
|
|
89
|
+
episode_of_care_obs.c.person_id,
|
|
90
|
+
episode_of_care_obs.c.episode_id,
|
|
91
|
+
)
|
|
92
|
+
.subquery(name="gp_referral")
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
pall_care_referral = (
|
|
96
|
+
sa.select(
|
|
97
|
+
episode_of_care_obs.c.person_id,
|
|
98
|
+
episode_of_care_obs.c.episode_id,
|
|
99
|
+
sa.func.min(episode_of_care_obs.c.event_date).label("first_pall_care_referral"),
|
|
100
|
+
)
|
|
101
|
+
.where(
|
|
102
|
+
episode_of_care_obs.c.event_concept_id
|
|
103
|
+
== runtime.cancer_procedures.cancer_consult_types.pall_care_referral # type: ignore[attr-defined]
|
|
104
|
+
)
|
|
105
|
+
.group_by(
|
|
106
|
+
episode_of_care_obs.c.person_id,
|
|
107
|
+
episode_of_care_obs.c.episode_id,
|
|
108
|
+
)
|
|
109
|
+
.subquery(name="pall_care_referral")
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
specialist_visit = (
|
|
113
|
+
sa.select(
|
|
114
|
+
DxRelevantVisitMV.person_id,
|
|
115
|
+
DxRelevantVisitMV.episode_id,
|
|
116
|
+
sa.func.min(DxRelevantVisitMV.visit_start_date).label("first_specialist_visit"),
|
|
117
|
+
)
|
|
118
|
+
.where(
|
|
119
|
+
DxRelevantVisitMV.provider_specialty_concept_id.in_(
|
|
120
|
+
[
|
|
121
|
+
runtime.cancer_procedures.encounter_provider_specialty.radonc, # type: ignore[attr-defined]
|
|
122
|
+
runtime.cancer_procedures.encounter_provider_specialty.medonc, # type: ignore[attr-defined]
|
|
123
|
+
runtime.cancer_procedures.encounter_provider_specialty.haematologist, # type: ignore[attr-defined]
|
|
124
|
+
]
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
.group_by(
|
|
128
|
+
DxRelevantVisitMV.person_id,
|
|
129
|
+
DxRelevantVisitMV.episode_id,
|
|
130
|
+
)
|
|
131
|
+
.subquery(name="specialist_visit")
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
pall_care_visit = (
|
|
135
|
+
sa.select(
|
|
136
|
+
DxRelevantVisitMV.person_id,
|
|
137
|
+
DxRelevantVisitMV.episode_id,
|
|
138
|
+
sa.func.min(DxRelevantVisitMV.visit_start_date).label("first_pall_care_visit"),
|
|
139
|
+
)
|
|
140
|
+
.where(
|
|
141
|
+
DxRelevantVisitMV.provider_specialty_concept_id
|
|
142
|
+
== runtime.cancer_procedures.encounter_provider_specialty.pall_care # type: ignore[attr-defined]
|
|
143
|
+
)
|
|
144
|
+
.group_by(
|
|
145
|
+
DxRelevantVisitMV.person_id,
|
|
146
|
+
DxRelevantVisitMV.episode_id,
|
|
147
|
+
)
|
|
148
|
+
.subquery(name="pall_care_visit")
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
specialist_trajectory = (
|
|
152
|
+
sa.select(
|
|
153
|
+
episode_of_care.c.person_id,
|
|
154
|
+
episode_of_care.c.episode_id,
|
|
155
|
+
episode_of_care.c.episode_start_date,
|
|
156
|
+
episode_of_care.c.episode_concept_id,
|
|
157
|
+
first_specialist.c.first_specialist_consult,
|
|
158
|
+
first_specialist.c.last_specialist_consult,
|
|
159
|
+
specialist_visit.c.first_specialist_visit,
|
|
160
|
+
gp_referral.c.initial_gp_referral,
|
|
161
|
+
pall_care_referral.c.first_pall_care_referral,
|
|
162
|
+
pall_care_visit.c.first_pall_care_visit,
|
|
163
|
+
)
|
|
164
|
+
.join(
|
|
165
|
+
first_specialist,
|
|
166
|
+
first_specialist.c.episode_id == episode_of_care.c.episode_id,
|
|
167
|
+
isouter=True,
|
|
168
|
+
)
|
|
169
|
+
.join(
|
|
170
|
+
gp_referral,
|
|
171
|
+
gp_referral.c.episode_id == episode_of_care.c.episode_id,
|
|
172
|
+
isouter=True,
|
|
173
|
+
)
|
|
174
|
+
.join(
|
|
175
|
+
pall_care_referral,
|
|
176
|
+
pall_care_referral.c.episode_id == episode_of_care.c.episode_id,
|
|
177
|
+
isouter=True,
|
|
178
|
+
)
|
|
179
|
+
.join(
|
|
180
|
+
specialist_visit,
|
|
181
|
+
specialist_visit.c.episode_id == episode_of_care.c.episode_id,
|
|
182
|
+
isouter=True,
|
|
183
|
+
)
|
|
184
|
+
.join(
|
|
185
|
+
pall_care_visit,
|
|
186
|
+
pall_care_visit.c.episode_id == episode_of_care.c.episode_id,
|
|
187
|
+
isouter=True,
|
|
188
|
+
)
|
|
189
|
+
.subquery(name="specialist_trajectory")
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
consult_window = (
|
|
193
|
+
sa.select(
|
|
194
|
+
sa.func.row_number().over().label("mv_id"),
|
|
195
|
+
specialist_trajectory.c.person_id,
|
|
196
|
+
specialist_trajectory.c.episode_id,
|
|
197
|
+
specialist_trajectory.c.episode_start_date,
|
|
198
|
+
specialist_trajectory.c.initial_gp_referral,
|
|
199
|
+
specialist_trajectory.c.episode_concept_id,
|
|
200
|
+
sa.func.least(
|
|
201
|
+
specialist_trajectory.c.first_specialist_consult,
|
|
202
|
+
specialist_trajectory.c.first_specialist_visit,
|
|
203
|
+
).label("first_specialist"),
|
|
204
|
+
sa.func.least(
|
|
205
|
+
specialist_trajectory.c.first_pall_care_referral,
|
|
206
|
+
specialist_trajectory.c.first_pall_care_visit,
|
|
207
|
+
).label("first_pall_care"),
|
|
208
|
+
sa.func.least(
|
|
209
|
+
specialist_trajectory.c.first_pall_care_referral,
|
|
210
|
+
specialist_trajectory.c.first_pall_care_visit,
|
|
211
|
+
TreatmentEnvelopeMV.earliest_treatment,
|
|
212
|
+
).label("first_pall_care_or_treatment"),
|
|
213
|
+
TreatmentEnvelopeMV.earliest_treatment,
|
|
214
|
+
sa.case(
|
|
215
|
+
(
|
|
216
|
+
sa.and_(
|
|
217
|
+
specialist_trajectory.c.initial_gp_referral.is_not(None),
|
|
218
|
+
sa.func.least(
|
|
219
|
+
specialist_trajectory.c.first_specialist_consult,
|
|
220
|
+
specialist_trajectory.c.first_specialist_visit,
|
|
221
|
+
).is_not(None),
|
|
222
|
+
),
|
|
223
|
+
days_between(
|
|
224
|
+
sa.func.least(
|
|
225
|
+
specialist_trajectory.c.first_specialist_consult,
|
|
226
|
+
specialist_trajectory.c.first_specialist_visit,
|
|
227
|
+
),
|
|
228
|
+
specialist_trajectory.c.initial_gp_referral,
|
|
229
|
+
),
|
|
230
|
+
),
|
|
231
|
+
else_=None,
|
|
232
|
+
).label("referral_to_specialist"),
|
|
233
|
+
sa.case(
|
|
234
|
+
(
|
|
235
|
+
sa.and_(
|
|
236
|
+
specialist_trajectory.c.initial_gp_referral.is_not(None),
|
|
237
|
+
sa.func.least(
|
|
238
|
+
specialist_trajectory.c.first_pall_care_referral,
|
|
239
|
+
specialist_trajectory.c.first_pall_care_visit,
|
|
240
|
+
TreatmentEnvelopeMV.earliest_treatment,
|
|
241
|
+
).is_not(None),
|
|
242
|
+
),
|
|
243
|
+
days_between(
|
|
244
|
+
sa.func.least(
|
|
245
|
+
specialist_trajectory.c.first_pall_care_referral,
|
|
246
|
+
specialist_trajectory.c.first_pall_care_visit,
|
|
247
|
+
TreatmentEnvelopeMV.earliest_treatment,
|
|
248
|
+
),
|
|
249
|
+
specialist_trajectory.c.initial_gp_referral,
|
|
250
|
+
),
|
|
251
|
+
),
|
|
252
|
+
else_=None,
|
|
253
|
+
).label("referral_to_tx"),
|
|
254
|
+
)
|
|
255
|
+
.join(
|
|
256
|
+
TreatmentEnvelopeMV,
|
|
257
|
+
TreatmentEnvelopeMV.condition_episode == specialist_trajectory.c.episode_id,
|
|
258
|
+
isouter=True,
|
|
259
|
+
)
|
|
260
|
+
.subquery(name="consult_window")
|
|
261
|
+
)
|
|
@@ -4,6 +4,7 @@ from orm_loader.helpers import Base
|
|
|
4
4
|
from datetime import date
|
|
5
5
|
from typing import Optional
|
|
6
6
|
from .treatment_summary_joins import condition_treatment_join
|
|
7
|
+
from .treatment_envelope_mv import TreatmentEnvelopeMV
|
|
7
8
|
from .course_mv import RTCourseMV
|
|
8
9
|
from .systemic_treatment_mv import SACTRegimenMV
|
|
9
10
|
from ...core.materialized import MaterializedViewMixin
|
|
@@ -44,4 +45,4 @@ class ConditionTreatmentEpisode(
|
|
|
44
45
|
sact_intent_concept_id: so.Mapped[int] = so.mapped_column(sa.Integer)
|
|
45
46
|
sact_intent_concept: so.Mapped[int] = so.mapped_column(sa.Integer)
|
|
46
47
|
exposure_count: so.Mapped[int] = so.mapped_column(sa.Integer)
|
|
47
|
-
regimen_count: so.Mapped[int] = so.mapped_column(sa.Integer)
|
|
48
|
+
regimen_count: so.Mapped[int] = so.mapped_column(sa.Integer)
|
{omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/events/__init__.py
RENAMED
|
@@ -18,6 +18,9 @@ from .dx_linked_procedure_mv import (
|
|
|
18
18
|
from .dx_linked_obs_mv import (
|
|
19
19
|
DxObservationMV,
|
|
20
20
|
)
|
|
21
|
+
from .dx_linked_visit_mv import (
|
|
22
|
+
DxRelevantVisitMV,
|
|
23
|
+
)
|
|
21
24
|
|
|
22
25
|
__all__ = [
|
|
23
26
|
"WeightDxMV",
|
|
@@ -33,4 +36,5 @@ __all__ = [
|
|
|
33
36
|
"DxMeasurementMV",
|
|
34
37
|
"DxProcedureMV",
|
|
35
38
|
"DxObservationMV",
|
|
36
|
-
|
|
39
|
+
"DxRelevantVisitMV",
|
|
40
|
+
]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import sqlalchemy as sa
|
|
2
|
+
import sqlalchemy.orm as so
|
|
3
|
+
from datetime import date, datetime
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from orm_loader.helpers import Base
|
|
7
|
+
|
|
8
|
+
from ...core.constructs import register_construct
|
|
9
|
+
from ...core.materialized import MaterializedViewMixin
|
|
10
|
+
from .visit_queries import dx_relevant_visits
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ConditionEpisodeVisitCols:
|
|
14
|
+
"""
|
|
15
|
+
Shared mapped columns for diagnosis-relevant visit materialized views.
|
|
16
|
+
"""
|
|
17
|
+
__table_args__ = {"extend_existing": True}
|
|
18
|
+
|
|
19
|
+
mv_id: so.Mapped[int] = so.mapped_column(primary_key=True)
|
|
20
|
+
|
|
21
|
+
person_id: so.Mapped[int] = so.mapped_column(sa.Integer)
|
|
22
|
+
visit_occurrence_id: so.Mapped[int] = so.mapped_column(sa.Integer)
|
|
23
|
+
visit_start_date: so.Mapped[date] = so.mapped_column(sa.Date)
|
|
24
|
+
visit_start_datetime: so.Mapped[Optional[datetime]] = so.mapped_column(
|
|
25
|
+
sa.DateTime, nullable=True
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
provider_id: so.Mapped[int] = so.mapped_column(sa.Integer)
|
|
29
|
+
provider_specialty: so.Mapped[Optional[str]] = so.mapped_column(
|
|
30
|
+
sa.String, nullable=True
|
|
31
|
+
)
|
|
32
|
+
provider_specialty_concept_id: so.Mapped[Optional[int]] = so.mapped_column(
|
|
33
|
+
sa.Integer, nullable=True
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
episode_id: so.Mapped[int] = so.mapped_column(sa.Integer)
|
|
37
|
+
episode_start_date: so.Mapped[date] = so.mapped_column(sa.Date)
|
|
38
|
+
|
|
39
|
+
episode_prior: so.Mapped[int] = so.mapped_column(sa.Integer)
|
|
40
|
+
diff_days: so.Mapped[float] = so.mapped_column(sa.Float)
|
|
41
|
+
rank: so.Mapped[int] = so.mapped_column(sa.Integer)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@register_construct
|
|
45
|
+
class DxRelevantVisitMV(ConditionEpisodeVisitCols, MaterializedViewMixin, Base):
|
|
46
|
+
"""
|
|
47
|
+
Provider-specialty visits linked to diagnosis episodes.
|
|
48
|
+
|
|
49
|
+
This view uses ranked episode assignment logic tailored to specialist visit
|
|
50
|
+
analysis rather than the generic event factory time-window attachment path.
|
|
51
|
+
"""
|
|
52
|
+
__mv_name__ = "dx_visit_mv"
|
|
53
|
+
__mv_select__ = dx_relevant_visits.select()
|
|
54
|
+
__mv_index__ = "person_id"
|
|
55
|
+
__deps__ = ()
|
|
56
|
+
__tablename__ = __mv_name__
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Query fragments for linking visit occurrences to disease episodes by specialty.
|
|
3
|
+
|
|
4
|
+
These queries support specialist consult-style measures where visit/provider
|
|
5
|
+
specialty is part of the derivation and generic event-attachment helpers are not
|
|
6
|
+
quite expressive enough.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import sqlalchemy as sa
|
|
10
|
+
import sqlalchemy.orm as so
|
|
11
|
+
|
|
12
|
+
from omop_alchemy.cdm.model import Concept, Episode, Provider, Visit_Occurrence
|
|
13
|
+
from omop_semantics.runtime.default_valuesets import runtime # type: ignore
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
provider_concept = so.aliased(Concept, name="provider_concept")
|
|
17
|
+
|
|
18
|
+
episode_of_care = (
|
|
19
|
+
sa.select(
|
|
20
|
+
Episode.person_id,
|
|
21
|
+
Episode.episode_id,
|
|
22
|
+
Episode.episode_start_date,
|
|
23
|
+
)
|
|
24
|
+
.where(
|
|
25
|
+
Episode.episode_concept_id
|
|
26
|
+
== runtime.types.disease_episode_types.episode_of_care # type: ignore[attr-defined]
|
|
27
|
+
)
|
|
28
|
+
.subquery(name="episode_of_care")
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
visit_start = sa.func.coalesce(
|
|
32
|
+
Visit_Occurrence.visit_start_datetime,
|
|
33
|
+
sa.cast(Visit_Occurrence.visit_start_date, sa.DateTime),
|
|
34
|
+
)
|
|
35
|
+
episode_start = sa.cast(episode_of_care.c.episode_start_date, sa.DateTime)
|
|
36
|
+
|
|
37
|
+
diff_days = sa.func.extract("epoch", visit_start - episode_start) / 86400.0
|
|
38
|
+
|
|
39
|
+
episode_start_prior = sa.case(
|
|
40
|
+
(
|
|
41
|
+
sa.func.abs(diff_days) < 180,
|
|
42
|
+
1,
|
|
43
|
+
),
|
|
44
|
+
(
|
|
45
|
+
visit_start > episode_start,
|
|
46
|
+
2,
|
|
47
|
+
),
|
|
48
|
+
else_=3,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
visits_by_specialty = (
|
|
52
|
+
sa.select(
|
|
53
|
+
Visit_Occurrence.person_id,
|
|
54
|
+
Visit_Occurrence.visit_occurrence_id,
|
|
55
|
+
Visit_Occurrence.visit_start_date,
|
|
56
|
+
Visit_Occurrence.visit_start_datetime,
|
|
57
|
+
Provider.provider_id,
|
|
58
|
+
provider_concept.concept_name.label("provider_specialty"),
|
|
59
|
+
provider_concept.concept_id.label("provider_specialty_concept_id"),
|
|
60
|
+
episode_of_care.c.episode_id,
|
|
61
|
+
episode_of_care.c.episode_start_date,
|
|
62
|
+
episode_start_prior.label("episode_prior"),
|
|
63
|
+
diff_days.label("diff_days"),
|
|
64
|
+
sa.func.row_number()
|
|
65
|
+
.over(
|
|
66
|
+
partition_by=Visit_Occurrence.visit_occurrence_id,
|
|
67
|
+
order_by=[episode_start_prior, diff_days],
|
|
68
|
+
)
|
|
69
|
+
.label("rank"),
|
|
70
|
+
)
|
|
71
|
+
.join(episode_of_care, episode_of_care.c.person_id == Visit_Occurrence.person_id)
|
|
72
|
+
.join(Provider, Visit_Occurrence.provider_id == Provider.provider_id)
|
|
73
|
+
.join(provider_concept, provider_concept.concept_id == Provider.specialty_concept_id)
|
|
74
|
+
.subquery(name="visits_by_specialty")
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
dx_relevant_visits = (
|
|
78
|
+
sa.select(
|
|
79
|
+
sa.func.row_number().over().label("mv_id"),
|
|
80
|
+
*visits_by_specialty.c,
|
|
81
|
+
)
|
|
82
|
+
.where(
|
|
83
|
+
sa.or_(
|
|
84
|
+
visits_by_specialty.c.episode_prior == 1,
|
|
85
|
+
visits_by_specialty.c.rank == 1,
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
.subquery(name="dx_relevant_visits")
|
|
89
|
+
)
|
|
@@ -1,14 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Resolver-backed query fragments for staging and condition modifier selection.
|
|
3
|
+
|
|
4
|
+
Importing this module is not a pure no-op. At module import time it resolves the
|
|
5
|
+
configured semantics-backed resolver sets and uses them to build the stage and
|
|
6
|
+
modifier query fragments that feed the modifier materialized views.
|
|
7
|
+
"""
|
|
8
|
+
|
|
1
9
|
from omop_constructs.semantics import registry
|
|
2
10
|
from omop_alchemy.cdm.model.clinical import Measurement
|
|
3
11
|
from omop_semantics.runtime.default_valuesets import runtime
|
|
4
12
|
from .modifier_factories import get_query_per_stage_type, get_eav_modifier_query, earliest_modifier, get_direct_modifier_query
|
|
5
|
-
"""
|
|
6
|
-
These calls have side effects of building the resolvers and querying the database to
|
|
7
|
-
cache the required concepts in memory, but this is necessary to create the correct
|
|
8
|
-
subqueries for staging modifiers.
|
|
9
13
|
|
|
10
|
-
TODO: we may want to refactor to separate out the resolver construction from the concept retrieval?
|
|
11
|
-
"""
|
|
12
14
|
t_stage_select = get_query_per_stage_type(registry['tnm_t_stage'].all_concepts, name="t_stage")
|
|
13
15
|
n_stage_select = get_query_per_stage_type(registry['tnm_n_stage'].all_concepts, name="n_stage")
|
|
14
16
|
m_stage_select = get_query_per_stage_type(registry['tnm_m_stage'].all_concepts, name="m_stage")
|
|
@@ -50,4 +52,4 @@ all_stage_select = get_direct_modifier_query(
|
|
|
50
52
|
)
|
|
51
53
|
),
|
|
52
54
|
name="all_stage_modifiers"
|
|
53
|
-
)
|
|
55
|
+
)
|
|
@@ -9,7 +9,11 @@ _CONSTRUCTS: Dict[str, Type[SupportsMaterializedView]] = {}
|
|
|
9
9
|
|
|
10
10
|
def register_construct(cls: T) -> T:
|
|
11
11
|
"""
|
|
12
|
-
|
|
12
|
+
Register a materialized view-backed construct class.
|
|
13
|
+
|
|
14
|
+
Registration is import-driven: any module that defines construct classes and
|
|
15
|
+
imports this decorator will add those classes to the in-process registry as
|
|
16
|
+
soon as the module is imported.
|
|
13
17
|
"""
|
|
14
18
|
name = getattr(cls, "__mv_name__", None)
|
|
15
19
|
if not name:
|
|
@@ -24,4 +28,12 @@ def register_construct(cls: T) -> T:
|
|
|
24
28
|
return cls
|
|
25
29
|
|
|
26
30
|
def get_construct_registry() -> ConstructRegistry:
|
|
27
|
-
|
|
31
|
+
"""
|
|
32
|
+
Return a registry view over all construct classes imported so far.
|
|
33
|
+
|
|
34
|
+
Notes
|
|
35
|
+
-----
|
|
36
|
+
The returned registry reflects the current Python process. If a construct
|
|
37
|
+
module has not yet been imported, its classes will not appear here.
|
|
38
|
+
"""
|
|
39
|
+
return ConstructRegistry(_CONSTRUCTS.values())
|
|
@@ -18,6 +18,9 @@ class MaterializedViewMixin:
|
|
|
18
18
|
|
|
19
19
|
@classmethod
|
|
20
20
|
def create_mv(cls, bind, *, with_data: bool = True) -> None:
|
|
21
|
+
"""
|
|
22
|
+
Create the backing materialized view for this mapped class.
|
|
23
|
+
"""
|
|
21
24
|
ddl = CreateMaterializedView(cls.__mv_name__, cls.__mv_select__, with_data=with_data)
|
|
22
25
|
bind.execute(ddl)
|
|
23
26
|
if cls.__mv_index__ is not None:
|
|
@@ -25,6 +28,9 @@ class MaterializedViewMixin:
|
|
|
25
28
|
|
|
26
29
|
@classmethod
|
|
27
30
|
def refresh_mv(cls, bind, *, concurrently: bool = False):
|
|
31
|
+
"""
|
|
32
|
+
Refresh the backing materialized view.
|
|
33
|
+
"""
|
|
28
34
|
suffix = " CONCURRENTLY" if concurrently else ""
|
|
29
35
|
bind.execute(
|
|
30
36
|
sa.text(f"REFRESH MATERIALIZED VIEW{suffix} {cls.__mv_name__}")
|
|
@@ -32,11 +38,17 @@ class MaterializedViewMixin:
|
|
|
32
38
|
|
|
33
39
|
@classmethod
|
|
34
40
|
def drop_mv(cls, bind, *, cascade: bool = False):
|
|
41
|
+
"""
|
|
42
|
+
Drop the backing materialized view.
|
|
43
|
+
"""
|
|
35
44
|
ddl = DropMaterializedView(cls.__mv_name__, cascade=cascade)
|
|
36
45
|
bind.execute(ddl)
|
|
37
46
|
|
|
38
47
|
@classmethod
|
|
39
48
|
def create_index(cls, bind, index_colname: str):
|
|
49
|
+
"""
|
|
50
|
+
Create the configured helper index for the materialized view if needed.
|
|
51
|
+
"""
|
|
40
52
|
bind.execute(sa.text(
|
|
41
53
|
f"CREATE INDEX IF NOT EXISTS {cls.__mv_name__}_{index_colname}_idx "
|
|
42
54
|
f"ON {cls.__mv_name__} ({index_colname})"
|
|
@@ -7,11 +7,20 @@ from .errors import DependencyCycleError
|
|
|
7
7
|
|
|
8
8
|
@dataclass(frozen=True)
|
|
9
9
|
class ConstructNode:
|
|
10
|
+
"""
|
|
11
|
+
Lightweight dependency node used when building construct execution plans.
|
|
12
|
+
"""
|
|
10
13
|
name: str
|
|
11
14
|
deps: tuple[str, ...] = ()
|
|
12
15
|
kind: str = "materialized_view"
|
|
13
16
|
|
|
14
17
|
def topo_sort(nodes: list[ConstructNode]) -> list[ConstructNode]:
|
|
18
|
+
"""
|
|
19
|
+
Topologically sort construct nodes by their declared dependencies.
|
|
20
|
+
|
|
21
|
+
Dependencies that point outside the provided node set are treated as
|
|
22
|
+
external and ignored for ordering purposes.
|
|
23
|
+
"""
|
|
15
24
|
by_name = {n.name: n for n in nodes}
|
|
16
25
|
indeg = {n.name: 0 for n in nodes}
|
|
17
26
|
adj = defaultdict(list)
|
|
@@ -9,12 +9,18 @@ from ..typing import SupportsMaterializedView
|
|
|
9
9
|
|
|
10
10
|
@dataclass(frozen=True)
|
|
11
11
|
class ConstructPlanItem:
|
|
12
|
+
"""
|
|
13
|
+
Serializable view of a construct's name, kind, and declared dependencies.
|
|
14
|
+
"""
|
|
12
15
|
name: str
|
|
13
16
|
kind: str
|
|
14
17
|
deps: tuple[str, ...]
|
|
15
18
|
|
|
16
19
|
|
|
17
20
|
def materialized_view_exists(bind, name: str, schema: str = "public") -> bool:
|
|
21
|
+
"""
|
|
22
|
+
Check whether a PostgreSQL materialized view already exists.
|
|
23
|
+
"""
|
|
18
24
|
sql = sa.text("""
|
|
19
25
|
SELECT EXISTS (
|
|
20
26
|
SELECT 1
|
|
@@ -34,15 +40,24 @@ class ConstructRegistry:
|
|
|
34
40
|
_constructs: dict[str, Type[SupportsMaterializedView]]
|
|
35
41
|
|
|
36
42
|
def __init__(self, constructs: Iterable[type[SupportsMaterializedView]]):
|
|
43
|
+
"""
|
|
44
|
+
Build a registry over an iterable of construct classes.
|
|
45
|
+
"""
|
|
37
46
|
self._constructs = {c.__mv_name__: c for c in constructs}
|
|
38
47
|
|
|
39
48
|
def __iter__(self) -> Iterator[type[SupportsMaterializedView]]:
|
|
40
49
|
return iter(self._constructs.values())
|
|
41
50
|
|
|
42
51
|
def get(self, name: str) -> type[SupportsMaterializedView]:
|
|
52
|
+
"""
|
|
53
|
+
Return a registered construct class by materialized view name.
|
|
54
|
+
"""
|
|
43
55
|
return self._constructs[name]
|
|
44
56
|
|
|
45
57
|
def plan(self) -> tuple[ConstructPlanItem, ...]:
|
|
58
|
+
"""
|
|
59
|
+
Return constructs in dependency order.
|
|
60
|
+
"""
|
|
46
61
|
nodes = [
|
|
47
62
|
ConstructNode(name=k, deps=v.__deps__, kind=getattr(v.__construct__, "kind", "materialized_view"))
|
|
48
63
|
for k, v in self._constructs.items()
|
|
@@ -51,6 +66,9 @@ class ConstructRegistry:
|
|
|
51
66
|
return tuple(ConstructPlanItem(n.name, n.kind, n.deps) for n in ordered)
|
|
52
67
|
|
|
53
68
|
def create_all(self, bind, *, with_data: bool = True):
|
|
69
|
+
"""
|
|
70
|
+
Create every registered materialized view in dependency order.
|
|
71
|
+
"""
|
|
54
72
|
for item in self.plan():
|
|
55
73
|
cls = self._constructs[item.name]
|
|
56
74
|
cls.create_mv(bind, with_data=with_data)
|
|
@@ -60,6 +78,9 @@ class ConstructRegistry:
|
|
|
60
78
|
self._constructs[item.name].refresh_mv(bind, concurrently=concurrently)
|
|
61
79
|
|
|
62
80
|
def drop_all(self, bind, *, cascade: bool = False) -> None:
|
|
81
|
+
"""
|
|
82
|
+
Drop every registered materialized view in reverse dependency order.
|
|
83
|
+
"""
|
|
63
84
|
# drop reverse order
|
|
64
85
|
for item in reversed(self.plan()):
|
|
65
86
|
try:
|
|
@@ -221,6 +242,9 @@ class ConstructRegistry:
|
|
|
221
242
|
|
|
222
243
|
|
|
223
244
|
def describe(self, full: bool = False) -> str:
|
|
245
|
+
"""
|
|
246
|
+
Render a text description of the registry dependency graph.
|
|
247
|
+
"""
|
|
224
248
|
try:
|
|
225
249
|
items = self.plan()
|
|
226
250
|
except Exception:
|
|
@@ -274,4 +298,4 @@ class ConstructRegistry:
|
|
|
274
298
|
last_root = i == len(roots) - 1
|
|
275
299
|
walk(root, "", last_root)
|
|
276
300
|
|
|
277
|
-
return "\n".join(lines)
|
|
301
|
+
return "\n".join(lines)
|
|
@@ -14,6 +14,9 @@ from typing import Callable
|
|
|
14
14
|
# as a pre-processing step
|
|
15
15
|
|
|
16
16
|
def get_concept_resolver_registry(engine: sa.engine.Engine) -> ConceptResolverRegistry:
|
|
17
|
+
"""
|
|
18
|
+
Create a resolver registry bound to the supplied SQLAlchemy engine.
|
|
19
|
+
"""
|
|
17
20
|
return ConceptResolverRegistry(engine)
|
|
18
21
|
|
|
19
22
|
|
|
@@ -25,6 +28,9 @@ def build_parent_resolver(
|
|
|
25
28
|
corrections: list[Callable] = [],
|
|
26
29
|
unknown_concept_id: int = UNKNOWN["generic"].concept_id
|
|
27
30
|
) -> ConceptResolver:
|
|
31
|
+
"""
|
|
32
|
+
Build a resolver from one or more OMOP parent concepts.
|
|
33
|
+
"""
|
|
28
34
|
return make_concept_resolver(
|
|
29
35
|
session,
|
|
30
36
|
name=resolver_name,
|
|
@@ -40,9 +46,12 @@ def build_stage_resolver(
|
|
|
40
46
|
parent_list: list[int],
|
|
41
47
|
stage_name: str
|
|
42
48
|
) -> ConceptResolver:
|
|
49
|
+
"""
|
|
50
|
+
Build a staging resolver with the standard TNM correction pipeline.
|
|
51
|
+
"""
|
|
43
52
|
return build_parent_resolver(
|
|
44
53
|
session,
|
|
45
54
|
resolver_name=f"tnm_{stage_name}_stage",
|
|
46
55
|
parent_list=parent_list,
|
|
47
56
|
corrections=[strip_uicc, make_stage],
|
|
48
|
-
)
|
|
57
|
+
)
|
{omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/semantics/runtime_lookups.py
RENAMED
|
@@ -54,16 +54,25 @@ DEFAULT_RESOLVER_BUILDERS = {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
def get_registry_engine(env_path: str = __file__):
|
|
57
|
+
"""
|
|
58
|
+
Build the engine used for runtime resolver lookup.
|
|
59
|
+
|
|
60
|
+
If ``ENGINE`` is not already defined in the environment, the function asks
|
|
61
|
+
``omop-alchemy`` to load environment configuration relative to ``env_path``.
|
|
62
|
+
"""
|
|
57
63
|
if os.environ.get("ENGINE") is None:
|
|
58
64
|
load_environment(env_path)
|
|
59
65
|
engine_string = get_engine_name()
|
|
60
66
|
return sa.create_engine(engine_string, future=True, echo=False)
|
|
61
67
|
|
|
62
68
|
def get_runtime_resolvers(engine: sa.Engine) -> ConceptResolverRegistry:
|
|
69
|
+
"""
|
|
70
|
+
Return a resolver registry populated with the default resolver builders.
|
|
71
|
+
"""
|
|
63
72
|
resolver_registry = get_concept_resolver_registry(engine)
|
|
64
73
|
|
|
65
74
|
# Register all known resolvers up-front (lazy build)
|
|
66
75
|
for name, builder in DEFAULT_RESOLVER_BUILDERS.items():
|
|
67
76
|
resolver_registry.register(name, builder)
|
|
68
77
|
|
|
69
|
-
return resolver_registry
|
|
78
|
+
return resolver_registry
|
|
@@ -4,6 +4,9 @@ from .core.plan import ConstructNode, topo_sort
|
|
|
4
4
|
|
|
5
5
|
@runtime_checkable
|
|
6
6
|
class SupportsMaterializedView(Protocol):
|
|
7
|
+
"""
|
|
8
|
+
Protocol for ORM classes backed by materialized view definitions.
|
|
9
|
+
"""
|
|
7
10
|
__mv_name__: str
|
|
8
11
|
__mv_select__: sa.sql.Select
|
|
9
12
|
__deps__: tuple[str, ...]
|
|
@@ -19,6 +22,9 @@ class SupportsMaterializedView(Protocol):
|
|
|
19
22
|
|
|
20
23
|
@runtime_checkable
|
|
21
24
|
class SupportsConstructRegistry(Protocol):
|
|
25
|
+
"""
|
|
26
|
+
Protocol for registry-like lifecycle managers.
|
|
27
|
+
"""
|
|
22
28
|
def create_all(self, bind: Any) -> None: ...
|
|
23
29
|
def refresh_all(
|
|
24
30
|
self,
|
|
@@ -31,12 +37,15 @@ class SupportsConstructRegistry(Protocol):
|
|
|
31
37
|
|
|
32
38
|
@runtime_checkable
|
|
33
39
|
class SupportsSelectable(Protocol):
|
|
40
|
+
"""
|
|
41
|
+
Protocol for objects that can emit a SQLAlchemy ``Select``.
|
|
42
|
+
"""
|
|
34
43
|
def select(self) -> sa.sql.Select: ...
|
|
35
44
|
|
|
36
45
|
|
|
37
46
|
@runtime_checkable
|
|
38
47
|
class SupportsQueryFactory(Protocol):
|
|
39
48
|
"""
|
|
40
|
-
Factory that produces SQLAlchemy
|
|
49
|
+
Factory that produces SQLAlchemy ``Select`` objects.
|
|
41
50
|
"""
|
|
42
51
|
def build(self) -> sa.sql.Select: ...
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/concepts/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/conditions/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/demography/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/episodes/course_joins.py
RENAMED
|
File without changes
|
{omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/episodes/course_mv.py
RENAMED
|
File without changes
|
{omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/episodes/cycle_join.py
RENAMED
|
File without changes
|
{omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/episodes/cycle_mv.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/episodes/fraction_mv.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/events/event_queries.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/modifiers/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{omop_constructs-0.4.0 → omop_constructs-0.4.2}/src/omop_constructs/alchemy/source_factories.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|