omop-semantics 0.2.2__tar.gz → 0.3.0__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_semantics-0.2.2 → omop_semantics-0.3.0}/PKG-INFO +2 -2
- {omop_semantics-0.2.2 → omop_semantics-0.3.0}/pyproject.toml +2 -2
- {omop_semantics-0.2.2 → omop_semantics-0.3.0}/src/omop_semantics/runtime/__init__.py +36 -0
- {omop_semantics-0.2.2 → omop_semantics-0.3.0}/src/omop_semantics/runtime/instance_loader.py +7 -0
- omop_semantics-0.3.0/src/omop_semantics/runtime/output_definitions.py +469 -0
- omop_semantics-0.3.0/src/omop_semantics/runtime/projection.py +211 -0
- {omop_semantics-0.2.2 → omop_semantics-0.3.0}/src/omop_semantics/runtime/resolver.py +31 -0
- omop_semantics-0.3.0/src/omop_semantics/schema/configuration/core/omop_profiles.yaml +81 -0
- {omop_semantics-0.2.2 → omop_semantics-0.3.0}/src/omop_semantics/schema/generated_models/omop_semantic_registry.py +13 -0
- {omop_semantics-0.2.2 → omop_semantics-0.3.0}/src/omop_semantics/schema/instances/profile_groups.yaml +44 -1
- omop_semantics-0.3.0/src/omop_semantics/schema/instances/profiles.yaml +102 -0
- omop_semantics-0.2.2/src/omop_semantics/schema/configuration/core/omop_profiles.yaml +0 -43
- omop_semantics-0.2.2/src/omop_semantics/schema/instances/profiles.yaml +0 -39
- {omop_semantics-0.2.2 → omop_semantics-0.3.0}/README.md +0 -0
- {omop_semantics-0.2.2 → omop_semantics-0.3.0}/src/omop_semantics/__init__.py +0 -0
- {omop_semantics-0.2.2 → omop_semantics-0.3.0}/src/omop_semantics/base.py +0 -0
- {omop_semantics-0.2.2 → omop_semantics-0.3.0}/src/omop_semantics/runtime/default_valuesets.py +0 -0
- {omop_semantics-0.2.2 → omop_semantics-0.3.0}/src/omop_semantics/runtime/renderers.py +0 -0
- {omop_semantics-0.2.2 → omop_semantics-0.3.0}/src/omop_semantics/runtime/unknown_handlers.py +0 -0
- {omop_semantics-0.2.2 → omop_semantics-0.3.0}/src/omop_semantics/runtime/value_sets.py +0 -0
- {omop_semantics-0.2.2 → omop_semantics-0.3.0}/src/omop_semantics/schema/__init__.py +0 -0
- {omop_semantics-0.2.2 → omop_semantics-0.3.0}/src/omop_semantics/schema/codegen.py +0 -0
- {omop_semantics-0.2.2 → omop_semantics-0.3.0}/src/omop_semantics/schema/configuration/core/omop_base.yaml +0 -0
- {omop_semantics-0.2.2 → omop_semantics-0.3.0}/src/omop_semantics/schema/configuration/core/omop_named_sets.yaml +0 -0
- {omop_semantics-0.2.2 → omop_semantics-0.3.0}/src/omop_semantics/schema/configuration/core/omop_templates.yaml +0 -0
- {omop_semantics-0.2.2 → omop_semantics-0.3.0}/src/omop_semantics/schema/configuration/core/omop_unknowns.yaml +0 -0
- {omop_semantics-0.2.2 → omop_semantics-0.3.0}/src/omop_semantics/schema/configuration/profiles/omop_episodes.yaml +0 -0
- {omop_semantics-0.2.2 → omop_semantics-0.3.0}/src/omop_semantics/schema/configuration/profiles/omop_modifiers.yaml +0 -0
- {omop_semantics-0.2.2 → omop_semantics-0.3.0}/src/omop_semantics/schema/configuration/profiles/omop_staging.yaml +0 -0
- {omop_semantics-0.2.2 → omop_semantics-0.3.0}/src/omop_semantics/schema/configuration/registry/omop_semantic_registry.yaml +0 -0
- {omop_semantics-0.2.2 → omop_semantics-0.3.0}/src/omop_semantics/schema/dump.py +0 -0
- {omop_semantics-0.2.2 → omop_semantics-0.3.0}/src/omop_semantics/schema/generated_models/__init__.py +0 -0
- {omop_semantics-0.2.2 → omop_semantics-0.3.0}/src/omop_semantics/schema/generated_models/omop_named_sets.py +0 -0
- {omop_semantics-0.2.2 → omop_semantics-0.3.0}/src/omop_semantics/schema/generated_models/template_set.py +0 -0
- {omop_semantics-0.2.2 → omop_semantics-0.3.0}/src/omop_semantics/schema/instances/demographic.yaml +0 -0
- {omop_semantics-0.2.2 → omop_semantics-0.3.0}/src/omop_semantics/schema/instances/enumerators.yaml +0 -0
- {omop_semantics-0.2.2 → omop_semantics-0.3.0}/src/omop_semantics/schema/instances/genomic.yaml +0 -0
- {omop_semantics-0.2.2 → omop_semantics-0.3.0}/src/omop_semantics/schema/instances/provider_specialty.yaml +0 -0
- {omop_semantics-0.2.2 → omop_semantics-0.3.0}/src/omop_semantics/schema/instances/valuesets.yaml +0 -0
- {omop_semantics-0.2.2 → omop_semantics-0.3.0}/src/omop_semantics/unknowns.py +0 -0
- {omop_semantics-0.2.2 → omop_semantics-0.3.0}/src/omop_semantics/utils/__init__.py +0 -0
- {omop_semantics-0.2.2 → omop_semantics-0.3.0}/src/omop_semantics/utils/paths.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: omop-semantics
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Define, validate, and use schema-backed semantic conventions for OMOP CDM
|
|
5
5
|
Author: gkennos
|
|
6
6
|
Author-email: gkennos <georgina.kennedy@unsw.edu.au>
|
|
@@ -17,7 +17,7 @@ Requires-Dist: pytest-cov>=4.0 ; extra == 'dev'
|
|
|
17
17
|
Requires-Dist: mypy>=1.8 ; extra == 'dev'
|
|
18
18
|
Requires-Dist: ruff>=0.4 ; extra == 'dev'
|
|
19
19
|
Requires-Dist: rich>=13.0 ; extra == 'dev'
|
|
20
|
-
Requires-Dist: tornado>=6.5.
|
|
20
|
+
Requires-Dist: tornado>=6.5.7 ; extra == 'dev'
|
|
21
21
|
Requires-Dist: pygments>=2.20.0 ; extra == 'dev'
|
|
22
22
|
Requires-Dist: mkdocs-material>=9.7.6 ; extra == 'docs'
|
|
23
23
|
Requires-Dist: mkdocstrings-python>=2.0.1 ; extra == 'docs'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "omop-semantics"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "Define, validate, and use schema-backed semantic conventions for OMOP CDM"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -28,7 +28,7 @@ dev = [
|
|
|
28
28
|
"mypy>=1.8",
|
|
29
29
|
"ruff>=0.4",
|
|
30
30
|
"rich>=13.0",
|
|
31
|
-
"tornado>=6.5.
|
|
31
|
+
"tornado>=6.5.7",
|
|
32
32
|
"pygments>=2.20.0"
|
|
33
33
|
]
|
|
34
34
|
docs = [
|
|
@@ -20,6 +20,26 @@ from .resolver import (
|
|
|
20
20
|
OmopSemanticObject,
|
|
21
21
|
SemanticProfileRuntime
|
|
22
22
|
)
|
|
23
|
+
from .projection import (
|
|
24
|
+
ProjectionProfileRuntime,
|
|
25
|
+
RuntimeProjectionProfile,
|
|
26
|
+
ProjectedOutputRow,
|
|
27
|
+
ProjectedOutputLink,
|
|
28
|
+
ProjectedOutputBundle,
|
|
29
|
+
SuppressedRow,
|
|
30
|
+
)
|
|
31
|
+
from .output_definitions import (
|
|
32
|
+
ContextFieldRef,
|
|
33
|
+
OutputRowProjection,
|
|
34
|
+
OutputLinkRule,
|
|
35
|
+
OutputDefinition,
|
|
36
|
+
CompiledOutputDefinition,
|
|
37
|
+
OutputDefinitionRuntime,
|
|
38
|
+
DerivationRule,
|
|
39
|
+
NO_DEFAULT,
|
|
40
|
+
SpecialValuePolicy,
|
|
41
|
+
SUPPRESSION_MODES,
|
|
42
|
+
)
|
|
23
43
|
from typing import TYPE_CHECKING
|
|
24
44
|
|
|
25
45
|
|
|
@@ -39,4 +59,20 @@ __all__ = [
|
|
|
39
59
|
"render_registry_fragment",
|
|
40
60
|
"render_profile_groups",
|
|
41
61
|
"SemanticProfileRuntime",
|
|
62
|
+
"ProjectionProfileRuntime",
|
|
63
|
+
"RuntimeProjectionProfile",
|
|
64
|
+
"ProjectedOutputRow",
|
|
65
|
+
"ProjectedOutputLink",
|
|
66
|
+
"ProjectedOutputBundle",
|
|
67
|
+
"SuppressedRow",
|
|
68
|
+
"ContextFieldRef",
|
|
69
|
+
"OutputRowProjection",
|
|
70
|
+
"OutputLinkRule",
|
|
71
|
+
"OutputDefinition",
|
|
72
|
+
"CompiledOutputDefinition",
|
|
73
|
+
"OutputDefinitionRuntime",
|
|
74
|
+
"DerivationRule",
|
|
75
|
+
"NO_DEFAULT",
|
|
76
|
+
"SpecialValuePolicy",
|
|
77
|
+
"SUPPRESSION_MODES",
|
|
42
78
|
]
|
|
@@ -45,6 +45,13 @@ def interpolate_profiles(group: dict, profiles: dict[str, OmopCdmProfile]) -> No
|
|
|
45
45
|
"cdm_table": profile.cdm_table,
|
|
46
46
|
"concept_slot": profile.concept_slot,
|
|
47
47
|
"value_slot": profile.value_slot,
|
|
48
|
+
"unit_slot": profile.unit_slot,
|
|
49
|
+
"operator_slot": profile.operator_slot,
|
|
50
|
+
"modifier_slot": profile.modifier_slot,
|
|
51
|
+
"extra_concept_slots": profile.extra_concept_slots,
|
|
52
|
+
"numeric_slots": profile.numeric_slots,
|
|
53
|
+
"string_slots": profile.string_slots,
|
|
54
|
+
"reference_slots": profile.reference_slots,
|
|
48
55
|
}
|
|
49
56
|
|
|
50
57
|
member.pop("concept_slot", None)
|
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, Iterable, Mapping, Sequence
|
|
5
|
+
|
|
6
|
+
from .projection import (
|
|
7
|
+
ProjectedOutputBundle,
|
|
8
|
+
ProjectedOutputLink,
|
|
9
|
+
ProjectedOutputRow,
|
|
10
|
+
ProjectionProfileRuntime,
|
|
11
|
+
RuntimeProjectionProfile,
|
|
12
|
+
SuppressedRow,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class ContextFieldRef:
|
|
18
|
+
"""
|
|
19
|
+
Reference to a value in the projection context.
|
|
20
|
+
|
|
21
|
+
The path is dot-delimited, for example:
|
|
22
|
+
|
|
23
|
+
- `grounded.concept_id`
|
|
24
|
+
- `source.numeric_value`
|
|
25
|
+
- `modifiers.intent_concept_id`
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
path: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
BindingValue = Any | ContextFieldRef
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class _NoDefault:
|
|
35
|
+
def __repr__(self) -> str:
|
|
36
|
+
return "NO_DEFAULT"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
NO_DEFAULT: Any = _NoDefault()
|
|
40
|
+
"""Sentinel distinguishing "no default configured" from a configured default of `None`."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class DerivationRule:
|
|
45
|
+
"""
|
|
46
|
+
Resolve one row's slot value from a source field other than the one that
|
|
47
|
+
grounded the row's entity concept, via a source-code lookup — with an
|
|
48
|
+
explicit escape hatch for codes that should suppress the row entirely.
|
|
49
|
+
|
|
50
|
+
This is the n:1 fan-in counterpart to `field_bindings` (which only ever
|
|
51
|
+
read from the same context a row's own entity concept was grounded from).
|
|
52
|
+
The canonical case is a diagnosis paired with a separately-collected
|
|
53
|
+
role/status field (e.g. Primary/Contributing/Non-contributing): the role
|
|
54
|
+
field is never grounded on its own, and its raw code either resolves
|
|
55
|
+
`condition_status_concept_id` via `code_map` or, for "Non-contributing",
|
|
56
|
+
drops the record entirely via `suppress_codes`.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
target_row: str
|
|
60
|
+
target_slot: str
|
|
61
|
+
source_field: ContextFieldRef
|
|
62
|
+
code_map: Mapping[str, Any] = field(default_factory=dict)
|
|
63
|
+
suppress_codes: frozenset[str] = frozenset()
|
|
64
|
+
default: Any = NO_DEFAULT
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
SUPPRESSION_MODES = frozenset({"drop", "fail", "keep_as_value", "keep_as_modifier"})
|
|
68
|
+
"""Recognized `SpecialValuePolicy.suppression_mode` values.
|
|
69
|
+
|
|
70
|
+
Only `drop` and `fail` have runtime behavior today — both are the degenerate,
|
|
71
|
+
single-field case validated against real data (a "meets criteria for X" Yes/No
|
|
72
|
+
item, where the negative answer carries no positive clinical content of its
|
|
73
|
+
own). `keep_as_value` and `keep_as_modifier` are named here per the schema
|
|
74
|
+
proposal so definitions can be authored against a stable enum, but they raise
|
|
75
|
+
`NotImplementedError` if actually triggered — there is no validated use case
|
|
76
|
+
yet for what they should bind.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(frozen=True)
|
|
81
|
+
class SpecialValuePolicy:
|
|
82
|
+
"""
|
|
83
|
+
Row-level suppression (or other handling) driven by the row's *own*
|
|
84
|
+
source value — the counterpart to `DerivationRule`, which reads a
|
|
85
|
+
different source field. Attach to `OutputRowProjection.special_value_policy`.
|
|
86
|
+
|
|
87
|
+
The canonical case is a Yes/No field phrased "meets criteria for X": Yes
|
|
88
|
+
grounds the row normally, No means nothing should be written for it. There
|
|
89
|
+
is no sibling field to consult, so no `DerivationRule` is involved — the
|
|
90
|
+
same value used to ground the row is checked against
|
|
91
|
+
`allowed_special_values`.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
source_field: ContextFieldRef
|
|
95
|
+
allowed_special_values: frozenset[str] = frozenset()
|
|
96
|
+
suppression_mode: str = "drop"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass(frozen=True)
|
|
100
|
+
class OutputRowProjection:
|
|
101
|
+
"""
|
|
102
|
+
Declarative row projection definition.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
row_id: str
|
|
106
|
+
profile_name: str
|
|
107
|
+
field_bindings: Mapping[str, BindingValue] = field(default_factory=dict)
|
|
108
|
+
defaults: Mapping[str, Any] = field(default_factory=dict)
|
|
109
|
+
required: bool = True
|
|
110
|
+
special_value_policy: SpecialValuePolicy | None = None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass(frozen=True)
|
|
114
|
+
class OutputLinkRule:
|
|
115
|
+
"""
|
|
116
|
+
Declarative relationship between projected rows.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
source_row: str
|
|
120
|
+
target_row: str
|
|
121
|
+
relationship_type: str
|
|
122
|
+
source_field: str | None = None
|
|
123
|
+
target_field: str | None = None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass(frozen=True)
|
|
127
|
+
class OutputDefinition:
|
|
128
|
+
"""
|
|
129
|
+
Minimal programmatic output definition.
|
|
130
|
+
|
|
131
|
+
This is intentionally additive and runtime-only for the first execution
|
|
132
|
+
slice. It provides enough structure to compile and deterministically
|
|
133
|
+
project rows before a dedicated YAML/schema authoring surface is added.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
name: str
|
|
137
|
+
role: str
|
|
138
|
+
template_names: tuple[str, ...] = ()
|
|
139
|
+
row_projections: tuple[OutputRowProjection, ...] = ()
|
|
140
|
+
link_rules: tuple[OutputLinkRule, ...] = ()
|
|
141
|
+
derivation_rules: tuple[DerivationRule, ...] = ()
|
|
142
|
+
notes: tuple[str, ...] = ()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@dataclass(frozen=True)
|
|
146
|
+
class CompiledRowProjection:
|
|
147
|
+
row_id: str
|
|
148
|
+
profile: RuntimeProjectionProfile
|
|
149
|
+
field_bindings: Mapping[str, BindingValue]
|
|
150
|
+
defaults: Mapping[str, Any]
|
|
151
|
+
required: bool = True
|
|
152
|
+
special_value_policy: SpecialValuePolicy | None = None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@dataclass(frozen=True)
|
|
156
|
+
class CompiledOutputDefinition:
|
|
157
|
+
name: str
|
|
158
|
+
role: str
|
|
159
|
+
template_names: tuple[str, ...]
|
|
160
|
+
row_projections: tuple[CompiledRowProjection, ...]
|
|
161
|
+
link_rules: tuple[OutputLinkRule, ...]
|
|
162
|
+
derivation_rules: tuple[DerivationRule, ...]
|
|
163
|
+
notes: tuple[str, ...]
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class OutputDefinitionRuntime:
|
|
167
|
+
"""
|
|
168
|
+
Compiler and executor for programmatic output definitions.
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
def __init__(
|
|
172
|
+
self,
|
|
173
|
+
definitions: Iterable[OutputDefinition],
|
|
174
|
+
projection_profiles: ProjectionProfileRuntime,
|
|
175
|
+
) -> None:
|
|
176
|
+
self.projection_profiles = projection_profiles
|
|
177
|
+
self._compiled_by_name: dict[str, CompiledOutputDefinition] = {}
|
|
178
|
+
self._compiled_by_template: dict[str, list[CompiledOutputDefinition]] = {}
|
|
179
|
+
for definition in definitions:
|
|
180
|
+
compiled = self._compile_definition(definition)
|
|
181
|
+
if compiled.name in self._compiled_by_name:
|
|
182
|
+
raise ValueError(f"Duplicate output definition name: '{compiled.name}'")
|
|
183
|
+
self._compiled_by_name[compiled.name] = compiled
|
|
184
|
+
for template_name in compiled.template_names:
|
|
185
|
+
self._compiled_by_template.setdefault(template_name, []).append(compiled)
|
|
186
|
+
|
|
187
|
+
def names(self) -> tuple[str, ...]:
|
|
188
|
+
return tuple(sorted(self._compiled_by_name))
|
|
189
|
+
|
|
190
|
+
def get(self, name: str) -> CompiledOutputDefinition:
|
|
191
|
+
return self._compiled_by_name[name]
|
|
192
|
+
|
|
193
|
+
def for_template(self, template_name: str) -> list[CompiledOutputDefinition]:
|
|
194
|
+
return list(self._compiled_by_template.get(template_name, ()))
|
|
195
|
+
|
|
196
|
+
def project(self, definition_name: str, context: Mapping[str, Any]) -> ProjectedOutputBundle:
|
|
197
|
+
compiled = self.get(definition_name)
|
|
198
|
+
projected_rows: list[ProjectedOutputRow] = []
|
|
199
|
+
row_ids: set[str] = set()
|
|
200
|
+
unresolved_fields: list[dict[str, Any]] = []
|
|
201
|
+
suppressed_rows: list[SuppressedRow] = []
|
|
202
|
+
audit_notes = list(compiled.notes)
|
|
203
|
+
|
|
204
|
+
for row_projection in compiled.row_projections:
|
|
205
|
+
resolved_fields = dict(row_projection.defaults)
|
|
206
|
+
missing_fields: list[str] = []
|
|
207
|
+
row_suppressed = False
|
|
208
|
+
|
|
209
|
+
for slot, binding in row_projection.field_bindings.items():
|
|
210
|
+
found, value = _resolve_binding(binding, context)
|
|
211
|
+
if not found:
|
|
212
|
+
missing_fields.append(slot)
|
|
213
|
+
continue
|
|
214
|
+
resolved_fields[slot] = value
|
|
215
|
+
|
|
216
|
+
for rule in compiled.derivation_rules:
|
|
217
|
+
if rule.target_row != row_projection.row_id:
|
|
218
|
+
continue
|
|
219
|
+
|
|
220
|
+
found, raw_value = _resolve_binding(rule.source_field, context)
|
|
221
|
+
if not found:
|
|
222
|
+
missing_fields.append(rule.target_slot)
|
|
223
|
+
continue
|
|
224
|
+
|
|
225
|
+
code = str(raw_value)
|
|
226
|
+
if code in rule.suppress_codes:
|
|
227
|
+
suppressed_rows.append(
|
|
228
|
+
SuppressedRow(
|
|
229
|
+
row_id=row_projection.row_id,
|
|
230
|
+
reason=(
|
|
231
|
+
f"derivation rule for '{rule.target_slot}' matched a "
|
|
232
|
+
f"suppress code on '{rule.source_field.path}'"
|
|
233
|
+
),
|
|
234
|
+
source_field=rule.source_field.path,
|
|
235
|
+
source_code=code,
|
|
236
|
+
)
|
|
237
|
+
)
|
|
238
|
+
row_suppressed = True
|
|
239
|
+
break
|
|
240
|
+
|
|
241
|
+
if code in rule.code_map:
|
|
242
|
+
resolved_fields[rule.target_slot] = rule.code_map[code]
|
|
243
|
+
elif rule.default is not NO_DEFAULT:
|
|
244
|
+
resolved_fields[rule.target_slot] = rule.default
|
|
245
|
+
else:
|
|
246
|
+
missing_fields.append(rule.target_slot)
|
|
247
|
+
|
|
248
|
+
policy = row_projection.special_value_policy
|
|
249
|
+
if policy is not None:
|
|
250
|
+
found, raw_value = _resolve_binding(policy.source_field, context)
|
|
251
|
+
if found:
|
|
252
|
+
code = str(raw_value)
|
|
253
|
+
if code in policy.allowed_special_values:
|
|
254
|
+
if policy.suppression_mode == "drop":
|
|
255
|
+
suppressed_rows.append(
|
|
256
|
+
SuppressedRow(
|
|
257
|
+
row_id=row_projection.row_id,
|
|
258
|
+
reason=(
|
|
259
|
+
"special_value_policy matched "
|
|
260
|
+
f"'{policy.source_field.path}' == '{code}'"
|
|
261
|
+
),
|
|
262
|
+
source_field=policy.source_field.path,
|
|
263
|
+
source_code=code,
|
|
264
|
+
)
|
|
265
|
+
)
|
|
266
|
+
row_suppressed = True
|
|
267
|
+
elif policy.suppression_mode == "fail":
|
|
268
|
+
raise ValueError(
|
|
269
|
+
f"row '{row_projection.row_id}' hit special value '{code}' "
|
|
270
|
+
f"on '{policy.source_field.path}', configured to fail"
|
|
271
|
+
)
|
|
272
|
+
else:
|
|
273
|
+
raise NotImplementedError(
|
|
274
|
+
f"suppression_mode '{policy.suppression_mode}' has no "
|
|
275
|
+
"runtime behavior yet"
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
if row_suppressed:
|
|
279
|
+
continue
|
|
280
|
+
|
|
281
|
+
if missing_fields:
|
|
282
|
+
unresolved_fields.append(
|
|
283
|
+
{
|
|
284
|
+
"row_id": row_projection.row_id,
|
|
285
|
+
"missing_fields": missing_fields,
|
|
286
|
+
"required": row_projection.required,
|
|
287
|
+
}
|
|
288
|
+
)
|
|
289
|
+
if row_projection.required:
|
|
290
|
+
continue
|
|
291
|
+
|
|
292
|
+
if row_projection.profile.concept_slot not in resolved_fields:
|
|
293
|
+
unresolved_fields.append(
|
|
294
|
+
{
|
|
295
|
+
"row_id": row_projection.row_id,
|
|
296
|
+
"missing_fields": [row_projection.profile.concept_slot],
|
|
297
|
+
"required": row_projection.required,
|
|
298
|
+
}
|
|
299
|
+
)
|
|
300
|
+
if row_projection.required:
|
|
301
|
+
continue
|
|
302
|
+
|
|
303
|
+
row = ProjectedOutputRow(
|
|
304
|
+
row_id=row_projection.row_id,
|
|
305
|
+
profile=row_projection.profile,
|
|
306
|
+
fields=resolved_fields,
|
|
307
|
+
)
|
|
308
|
+
projected_rows.append(row)
|
|
309
|
+
row_ids.add(row.row_id)
|
|
310
|
+
|
|
311
|
+
links: list[ProjectedOutputLink] = []
|
|
312
|
+
for link_rule in compiled.link_rules:
|
|
313
|
+
if link_rule.source_row in row_ids and link_rule.target_row in row_ids:
|
|
314
|
+
links.append(
|
|
315
|
+
ProjectedOutputLink(
|
|
316
|
+
source_row=link_rule.source_row,
|
|
317
|
+
target_row=link_rule.target_row,
|
|
318
|
+
relationship_type=link_rule.relationship_type,
|
|
319
|
+
source_field=link_rule.source_field,
|
|
320
|
+
target_field=link_rule.target_field,
|
|
321
|
+
)
|
|
322
|
+
)
|
|
323
|
+
else:
|
|
324
|
+
unresolved_fields.append(
|
|
325
|
+
{
|
|
326
|
+
"link": {
|
|
327
|
+
"source_row": link_rule.source_row,
|
|
328
|
+
"target_row": link_rule.target_row,
|
|
329
|
+
"relationship_type": link_rule.relationship_type,
|
|
330
|
+
},
|
|
331
|
+
"missing_rows": [
|
|
332
|
+
row_id
|
|
333
|
+
for row_id in (link_rule.source_row, link_rule.target_row)
|
|
334
|
+
if row_id not in row_ids
|
|
335
|
+
],
|
|
336
|
+
}
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
return ProjectedOutputBundle(
|
|
340
|
+
definition_name=compiled.name,
|
|
341
|
+
role=compiled.role,
|
|
342
|
+
rows=projected_rows,
|
|
343
|
+
links=links,
|
|
344
|
+
unresolved_fields=unresolved_fields,
|
|
345
|
+
suppressed_rows=suppressed_rows,
|
|
346
|
+
audit_notes=audit_notes,
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
def project_for_template(
|
|
350
|
+
self,
|
|
351
|
+
template_name: str,
|
|
352
|
+
context: Mapping[str, Any],
|
|
353
|
+
*,
|
|
354
|
+
definition_name: str | None = None,
|
|
355
|
+
) -> ProjectedOutputBundle:
|
|
356
|
+
matches = self.for_template(template_name)
|
|
357
|
+
if definition_name is not None:
|
|
358
|
+
matches = [item for item in matches if item.name == definition_name]
|
|
359
|
+
if not matches:
|
|
360
|
+
raise KeyError(f"No output definition found for template '{template_name}'")
|
|
361
|
+
if len(matches) > 1:
|
|
362
|
+
names = ", ".join(sorted(item.name for item in matches))
|
|
363
|
+
raise ValueError(
|
|
364
|
+
f"Multiple output definitions match template '{template_name}': {names}"
|
|
365
|
+
)
|
|
366
|
+
return self.project(matches[0].name, context)
|
|
367
|
+
|
|
368
|
+
def _compile_definition(self, definition: OutputDefinition) -> CompiledOutputDefinition:
|
|
369
|
+
row_ids: set[str] = set()
|
|
370
|
+
compiled_rows: list[CompiledRowProjection] = []
|
|
371
|
+
profile_by_row: dict[str, RuntimeProjectionProfile] = {}
|
|
372
|
+
errors: list[str] = []
|
|
373
|
+
|
|
374
|
+
for row_projection in definition.row_projections:
|
|
375
|
+
if row_projection.row_id in row_ids:
|
|
376
|
+
errors.append(f"duplicate row_id '{row_projection.row_id}'")
|
|
377
|
+
continue
|
|
378
|
+
row_ids.add(row_projection.row_id)
|
|
379
|
+
|
|
380
|
+
profile = self.projection_profiles.get(row_projection.profile_name)
|
|
381
|
+
declared_slots = set(row_projection.defaults) | set(row_projection.field_bindings)
|
|
382
|
+
invalid_slots = sorted(slot for slot in declared_slots if not profile.allows_slot(slot))
|
|
383
|
+
if invalid_slots:
|
|
384
|
+
errors.append(
|
|
385
|
+
f"row '{row_projection.row_id}' uses slots not present in profile "
|
|
386
|
+
f"'{profile.name}': {invalid_slots}"
|
|
387
|
+
)
|
|
388
|
+
continue
|
|
389
|
+
|
|
390
|
+
policy = row_projection.special_value_policy
|
|
391
|
+
if policy is not None and policy.suppression_mode not in SUPPRESSION_MODES:
|
|
392
|
+
errors.append(
|
|
393
|
+
f"row '{row_projection.row_id}' has special_value_policy with "
|
|
394
|
+
f"unrecognized suppression_mode '{policy.suppression_mode}'; "
|
|
395
|
+
f"expected one of {sorted(SUPPRESSION_MODES)}"
|
|
396
|
+
)
|
|
397
|
+
continue
|
|
398
|
+
|
|
399
|
+
profile_by_row[row_projection.row_id] = profile
|
|
400
|
+
compiled_rows.append(
|
|
401
|
+
CompiledRowProjection(
|
|
402
|
+
row_id=row_projection.row_id,
|
|
403
|
+
profile=profile,
|
|
404
|
+
field_bindings=dict(row_projection.field_bindings),
|
|
405
|
+
defaults=dict(row_projection.defaults),
|
|
406
|
+
required=row_projection.required,
|
|
407
|
+
special_value_policy=policy,
|
|
408
|
+
)
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
for link_rule in definition.link_rules:
|
|
412
|
+
if link_rule.source_row not in row_ids:
|
|
413
|
+
errors.append(
|
|
414
|
+
f"link rule references unknown source_row '{link_rule.source_row}'"
|
|
415
|
+
)
|
|
416
|
+
if link_rule.target_row not in row_ids:
|
|
417
|
+
errors.append(
|
|
418
|
+
f"link rule references unknown target_row '{link_rule.target_row}'"
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
for derivation_rule in definition.derivation_rules:
|
|
422
|
+
if derivation_rule.target_row not in row_ids:
|
|
423
|
+
errors.append(
|
|
424
|
+
"derivation rule references unknown target_row "
|
|
425
|
+
f"'{derivation_rule.target_row}'"
|
|
426
|
+
)
|
|
427
|
+
continue
|
|
428
|
+
profile = profile_by_row.get(derivation_rule.target_row)
|
|
429
|
+
if profile is not None and not profile.allows_slot(derivation_rule.target_slot):
|
|
430
|
+
errors.append(
|
|
431
|
+
f"derivation rule for row '{derivation_rule.target_row}' targets slot "
|
|
432
|
+
f"'{derivation_rule.target_slot}' not present in profile '{profile.name}'"
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
if errors:
|
|
436
|
+
raise ValueError(
|
|
437
|
+
f"Output definition '{definition.name}' failed validation:\n"
|
|
438
|
+
+ "\n".join(f" - {item}" for item in errors)
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
return CompiledOutputDefinition(
|
|
442
|
+
name=definition.name,
|
|
443
|
+
role=definition.role,
|
|
444
|
+
template_names=tuple(definition.template_names),
|
|
445
|
+
row_projections=tuple(compiled_rows),
|
|
446
|
+
link_rules=tuple(definition.link_rules),
|
|
447
|
+
derivation_rules=tuple(definition.derivation_rules),
|
|
448
|
+
notes=tuple(definition.notes),
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _resolve_binding(binding: BindingValue, context: Mapping[str, Any]) -> tuple[bool, Any]:
|
|
453
|
+
if isinstance(binding, ContextFieldRef):
|
|
454
|
+
return _resolve_context_path(context, binding.path)
|
|
455
|
+
return True, binding
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _resolve_context_path(context: Mapping[str, Any], path: str) -> tuple[bool, Any]:
|
|
459
|
+
current: Any = context
|
|
460
|
+
for part in path.split("."):
|
|
461
|
+
if isinstance(current, Mapping):
|
|
462
|
+
if part not in current:
|
|
463
|
+
return False, None
|
|
464
|
+
current = current[part]
|
|
465
|
+
continue
|
|
466
|
+
if not hasattr(current, part):
|
|
467
|
+
return False, None
|
|
468
|
+
current = getattr(current, part)
|
|
469
|
+
return True, current
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, Mapping
|
|
5
|
+
|
|
6
|
+
from omop_semantics.schema.generated_models.omop_semantic_registry import OmopCdmProfile
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class RuntimeProjectionProfile:
|
|
11
|
+
"""
|
|
12
|
+
Attribute-based runtime view over an `OmopCdmProfile`.
|
|
13
|
+
|
|
14
|
+
This is the structural counterpart to `RuntimeTemplate`: it exposes
|
|
15
|
+
the CDM row-shape information in a stable, ergonomic form for
|
|
16
|
+
downstream execution layers that need to reason about units,
|
|
17
|
+
operators, inline modifiers, or other supported slots.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
name: str
|
|
21
|
+
cdm_table: str
|
|
22
|
+
concept_slot: str
|
|
23
|
+
value_slot: str | None = None
|
|
24
|
+
unit_slot: str | None = None
|
|
25
|
+
operator_slot: str | None = None
|
|
26
|
+
modifier_slot: str | None = None
|
|
27
|
+
extra_concept_slots: tuple[str, ...] = ()
|
|
28
|
+
numeric_slots: tuple[str, ...] = ()
|
|
29
|
+
string_slots: tuple[str, ...] = ()
|
|
30
|
+
reference_slots: tuple[str, ...] = ()
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def from_profile(cls, profile: OmopCdmProfile) -> "RuntimeProjectionProfile":
|
|
34
|
+
return cls(
|
|
35
|
+
name=profile.name,
|
|
36
|
+
cdm_table=profile.cdm_table,
|
|
37
|
+
concept_slot=profile.concept_slot,
|
|
38
|
+
value_slot=profile.value_slot,
|
|
39
|
+
unit_slot=profile.unit_slot,
|
|
40
|
+
operator_slot=profile.operator_slot,
|
|
41
|
+
modifier_slot=profile.modifier_slot,
|
|
42
|
+
extra_concept_slots=tuple(profile.extra_concept_slots or ()),
|
|
43
|
+
numeric_slots=tuple(profile.numeric_slots or ()),
|
|
44
|
+
string_slots=tuple(profile.string_slots or ()),
|
|
45
|
+
reference_slots=tuple(profile.reference_slots or ()),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def concept_slots(self) -> tuple[str, ...]:
|
|
49
|
+
slots: list[str] = [self.concept_slot]
|
|
50
|
+
if self.modifier_slot is not None:
|
|
51
|
+
slots.append(self.modifier_slot)
|
|
52
|
+
slots.extend(self.extra_concept_slots)
|
|
53
|
+
if self.unit_slot is not None:
|
|
54
|
+
slots.append(self.unit_slot)
|
|
55
|
+
if self.operator_slot is not None:
|
|
56
|
+
slots.append(self.operator_slot)
|
|
57
|
+
return tuple(slots)
|
|
58
|
+
|
|
59
|
+
def all_slots(self) -> tuple[str, ...]:
|
|
60
|
+
slots: list[str] = [self.concept_slot]
|
|
61
|
+
if self.value_slot is not None:
|
|
62
|
+
slots.append(self.value_slot)
|
|
63
|
+
if self.unit_slot is not None:
|
|
64
|
+
slots.append(self.unit_slot)
|
|
65
|
+
if self.operator_slot is not None:
|
|
66
|
+
slots.append(self.operator_slot)
|
|
67
|
+
if self.modifier_slot is not None:
|
|
68
|
+
slots.append(self.modifier_slot)
|
|
69
|
+
slots.extend(self.extra_concept_slots)
|
|
70
|
+
slots.extend(self.numeric_slots)
|
|
71
|
+
slots.extend(self.string_slots)
|
|
72
|
+
slots.extend(self.reference_slots)
|
|
73
|
+
return tuple(slots)
|
|
74
|
+
|
|
75
|
+
def allows_slot(self, slot: str) -> bool:
|
|
76
|
+
return slot in self.all_slots()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class ProjectionProfileRuntime:
|
|
80
|
+
"""
|
|
81
|
+
Indexed runtime access to compiled CDM projection profiles.
|
|
82
|
+
|
|
83
|
+
This keeps the structural profile catalogue available at runtime
|
|
84
|
+
even when execution is not yet using higher-level output definitions.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def __init__(self, profiles: Mapping[str, OmopCdmProfile]):
|
|
88
|
+
self._profiles = dict(profiles)
|
|
89
|
+
|
|
90
|
+
def names(self) -> tuple[str, ...]:
|
|
91
|
+
return tuple(sorted(self._profiles))
|
|
92
|
+
|
|
93
|
+
def get(self, name: str) -> RuntimeProjectionProfile:
|
|
94
|
+
return RuntimeProjectionProfile.from_profile(self._profiles[name])
|
|
95
|
+
|
|
96
|
+
def by_table(self, cdm_table: str) -> list[RuntimeProjectionProfile]:
|
|
97
|
+
return [
|
|
98
|
+
RuntimeProjectionProfile.from_profile(profile)
|
|
99
|
+
for profile in self._profiles.values()
|
|
100
|
+
if profile.cdm_table == cdm_table
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass(frozen=True)
|
|
105
|
+
class ProjectedOutputRow:
|
|
106
|
+
"""
|
|
107
|
+
One projected row produced by a deterministic semantic execution step.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
row_id: str
|
|
111
|
+
profile: RuntimeProjectionProfile
|
|
112
|
+
fields: dict[str, Any]
|
|
113
|
+
|
|
114
|
+
def __post_init__(self) -> None:
|
|
115
|
+
unknown_slots = sorted(set(self.fields) - set(self.profile.all_slots()))
|
|
116
|
+
if unknown_slots:
|
|
117
|
+
raise ValueError(
|
|
118
|
+
f"Projected row '{self.row_id}' contains fields not allowed by "
|
|
119
|
+
f"profile '{self.profile.name}': {unknown_slots}"
|
|
120
|
+
)
|
|
121
|
+
if self.profile.concept_slot not in self.fields:
|
|
122
|
+
raise ValueError(
|
|
123
|
+
f"Projected row '{self.row_id}' is missing required concept slot "
|
|
124
|
+
f"'{self.profile.concept_slot}' for profile '{self.profile.name}'"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass(frozen=True)
|
|
129
|
+
class SuppressedRow:
|
|
130
|
+
"""
|
|
131
|
+
A row a definition would have produced, dropped deterministically by a
|
|
132
|
+
`DerivationRule` or row-level suppression policy rather than fabricated
|
|
133
|
+
with a missing or null slot value.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
row_id: str
|
|
137
|
+
reason: str
|
|
138
|
+
source_field: str
|
|
139
|
+
source_code: str
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass(frozen=True)
|
|
143
|
+
class ProjectedOutputLink:
|
|
144
|
+
"""
|
|
145
|
+
Relationship between projected rows.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
source_row: str
|
|
149
|
+
target_row: str
|
|
150
|
+
relationship_type: str
|
|
151
|
+
source_field: str | None = None
|
|
152
|
+
target_field: str | None = None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@dataclass(frozen=True)
|
|
156
|
+
class ProjectedOutputBundle:
|
|
157
|
+
"""
|
|
158
|
+
Transport-friendly deterministic projection result.
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
definition_name: str
|
|
162
|
+
role: str
|
|
163
|
+
rows: list[ProjectedOutputRow]
|
|
164
|
+
links: list[ProjectedOutputLink] = field(default_factory=list)
|
|
165
|
+
constraint_checks: list[dict[str, Any]] = field(default_factory=list)
|
|
166
|
+
unresolved_fields: list[dict[str, Any]] = field(default_factory=list)
|
|
167
|
+
suppressed_rows: list[SuppressedRow] = field(default_factory=list)
|
|
168
|
+
audit_notes: list[str] = field(default_factory=list)
|
|
169
|
+
|
|
170
|
+
def row(self, row_id: str) -> ProjectedOutputRow:
|
|
171
|
+
for row in self.rows:
|
|
172
|
+
if row.row_id == row_id:
|
|
173
|
+
return row
|
|
174
|
+
raise KeyError(f"No projected row with id '{row_id}'")
|
|
175
|
+
|
|
176
|
+
def to_dict(self) -> dict[str, Any]:
|
|
177
|
+
return {
|
|
178
|
+
"definition_name": self.definition_name,
|
|
179
|
+
"role": self.role,
|
|
180
|
+
"rows": [
|
|
181
|
+
{
|
|
182
|
+
"row_id": row.row_id,
|
|
183
|
+
"profile": row.profile.name,
|
|
184
|
+
"cdm_table": row.profile.cdm_table,
|
|
185
|
+
"fields": dict(row.fields),
|
|
186
|
+
}
|
|
187
|
+
for row in self.rows
|
|
188
|
+
],
|
|
189
|
+
"links": [
|
|
190
|
+
{
|
|
191
|
+
"source_row": link.source_row,
|
|
192
|
+
"target_row": link.target_row,
|
|
193
|
+
"relationship_type": link.relationship_type,
|
|
194
|
+
"source_field": link.source_field,
|
|
195
|
+
"target_field": link.target_field,
|
|
196
|
+
}
|
|
197
|
+
for link in self.links
|
|
198
|
+
],
|
|
199
|
+
"constraint_checks": [dict(item) for item in self.constraint_checks],
|
|
200
|
+
"unresolved_fields": [dict(item) for item in self.unresolved_fields],
|
|
201
|
+
"suppressed_rows": [
|
|
202
|
+
{
|
|
203
|
+
"row_id": row.row_id,
|
|
204
|
+
"reason": row.reason,
|
|
205
|
+
"source_field": row.source_field,
|
|
206
|
+
"source_code": row.source_code,
|
|
207
|
+
}
|
|
208
|
+
for row in self.suppressed_rows
|
|
209
|
+
],
|
|
210
|
+
"audit_notes": list(self.audit_notes),
|
|
211
|
+
}
|
|
@@ -22,6 +22,8 @@ from .instance_loader import (
|
|
|
22
22
|
needs_profile_interpolation,
|
|
23
23
|
)
|
|
24
24
|
from omop_semantics.utils.paths import INSTANCE_DIR
|
|
25
|
+
from .projection import ProjectionProfileRuntime, RuntimeProjectionProfile
|
|
26
|
+
from .output_definitions import OutputDefinition, OutputDefinitionRuntime
|
|
25
27
|
|
|
26
28
|
|
|
27
29
|
class CompiledTemplate(TypedDict):
|
|
@@ -203,6 +205,13 @@ class RuntimeTemplate:
|
|
|
203
205
|
value_concept_ids=c["value_concept_ids"],
|
|
204
206
|
)
|
|
205
207
|
|
|
208
|
+
@property
|
|
209
|
+
def projection_profile(self) -> RuntimeProjectionProfile:
|
|
210
|
+
"""
|
|
211
|
+
Structural runtime view of the underlying CDM profile.
|
|
212
|
+
"""
|
|
213
|
+
return RuntimeProjectionProfile.from_profile(self.cdm_profile)
|
|
214
|
+
|
|
206
215
|
|
|
207
216
|
@dataclass(frozen=True)
|
|
208
217
|
class RegistryRuntimeDiff:
|
|
@@ -999,6 +1008,7 @@ class OmopSemanticEngine:
|
|
|
999
1008
|
self,
|
|
1000
1009
|
registry_fragment: RegistryFragment,
|
|
1001
1010
|
profile_objects: dict[str, dict] | None = None,
|
|
1011
|
+
cdm_profiles: dict[str, OmopCdmProfile] | None = None,
|
|
1002
1012
|
):
|
|
1003
1013
|
"""
|
|
1004
1014
|
Construct a semantic engine from an already-loaded registry fragment.
|
|
@@ -1022,6 +1032,11 @@ class OmopSemanticEngine:
|
|
|
1022
1032
|
if profile_objects is not None
|
|
1023
1033
|
else None
|
|
1024
1034
|
)
|
|
1035
|
+
self.projection_profiles = (
|
|
1036
|
+
ProjectionProfileRuntime(cdm_profiles)
|
|
1037
|
+
if cdm_profiles is not None
|
|
1038
|
+
else None
|
|
1039
|
+
)
|
|
1025
1040
|
|
|
1026
1041
|
@classmethod
|
|
1027
1042
|
def from_instances(cls, fragment: RegistryFragment) -> "OmopSemanticEngine":
|
|
@@ -1098,6 +1113,7 @@ class OmopSemanticEngine:
|
|
|
1098
1113
|
return cls(
|
|
1099
1114
|
registry_fragment=merged_fragment,
|
|
1100
1115
|
profile_objects=profile_objects or None,
|
|
1116
|
+
cdm_profiles=cdm_profiles,
|
|
1101
1117
|
)
|
|
1102
1118
|
|
|
1103
1119
|
def docs_html(self) -> Html:
|
|
@@ -1122,3 +1138,18 @@ class OmopSemanticEngine:
|
|
|
1122
1138
|
])
|
|
1123
1139
|
|
|
1124
1140
|
return Html("".join(parts))
|
|
1141
|
+
|
|
1142
|
+
def build_output_definition_runtime(
|
|
1143
|
+
self,
|
|
1144
|
+
definitions: Iterable[OutputDefinition],
|
|
1145
|
+
) -> OutputDefinitionRuntime:
|
|
1146
|
+
"""
|
|
1147
|
+
Compile programmatic output definitions against the loaded profile catalogue.
|
|
1148
|
+
|
|
1149
|
+
This is the additive bridge from structural CDM profiles to deterministic
|
|
1150
|
+
output bundles. The first implementation slice is intentionally runtime-only
|
|
1151
|
+
and does not require a dedicated YAML schema for output definitions.
|
|
1152
|
+
"""
|
|
1153
|
+
if self.projection_profiles is None:
|
|
1154
|
+
raise RuntimeError("Projection profiles are not available on this semantic engine")
|
|
1155
|
+
return OutputDefinitionRuntime(definitions, self.projection_profiles)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
id: https://example.org/omop_semantics/cdm_profiles
|
|
2
|
+
name: cdm_profiles
|
|
3
|
+
|
|
4
|
+
imports:
|
|
5
|
+
- omop_base
|
|
6
|
+
|
|
7
|
+
classes:
|
|
8
|
+
CDMProfiles:
|
|
9
|
+
slots:
|
|
10
|
+
- profiles
|
|
11
|
+
tree_root: true
|
|
12
|
+
OmopCdmProfile:
|
|
13
|
+
slots:
|
|
14
|
+
- name
|
|
15
|
+
- cdm_table
|
|
16
|
+
- concept_slot
|
|
17
|
+
- value_slot
|
|
18
|
+
- unit_slot
|
|
19
|
+
- operator_slot
|
|
20
|
+
- modifier_slot
|
|
21
|
+
- extra_concept_slots
|
|
22
|
+
- numeric_slots
|
|
23
|
+
- string_slots
|
|
24
|
+
- reference_slots
|
|
25
|
+
slots:
|
|
26
|
+
profiles:
|
|
27
|
+
range: OmopCdmProfile
|
|
28
|
+
multivalued: true
|
|
29
|
+
name:
|
|
30
|
+
range: string
|
|
31
|
+
required: true
|
|
32
|
+
cdm_table:
|
|
33
|
+
range: CdmTable
|
|
34
|
+
required: true
|
|
35
|
+
concept_slot:
|
|
36
|
+
range: string
|
|
37
|
+
required: true
|
|
38
|
+
value_slot:
|
|
39
|
+
range: string
|
|
40
|
+
required: false
|
|
41
|
+
unit_slot:
|
|
42
|
+
range: string
|
|
43
|
+
required: false
|
|
44
|
+
operator_slot:
|
|
45
|
+
range: string
|
|
46
|
+
required: false
|
|
47
|
+
modifier_slot:
|
|
48
|
+
range: string
|
|
49
|
+
required: false
|
|
50
|
+
extra_concept_slots:
|
|
51
|
+
range: string
|
|
52
|
+
multivalued: true
|
|
53
|
+
required: false
|
|
54
|
+
numeric_slots:
|
|
55
|
+
range: string
|
|
56
|
+
multivalued: true
|
|
57
|
+
required: false
|
|
58
|
+
string_slots:
|
|
59
|
+
range: string
|
|
60
|
+
multivalued: true
|
|
61
|
+
required: false
|
|
62
|
+
reference_slots:
|
|
63
|
+
range: string
|
|
64
|
+
multivalued: true
|
|
65
|
+
required: false
|
|
66
|
+
enums:
|
|
67
|
+
CdmTable:
|
|
68
|
+
description: OMOP CDM table to which a semantic template applies
|
|
69
|
+
permissible_values:
|
|
70
|
+
observation:
|
|
71
|
+
measurement:
|
|
72
|
+
drug_exposure:
|
|
73
|
+
procedure_occurrence:
|
|
74
|
+
condition_occurrence:
|
|
75
|
+
device_exposure:
|
|
76
|
+
visit_occurrence:
|
|
77
|
+
death:
|
|
78
|
+
specimen:
|
|
79
|
+
fact_relationship:
|
|
80
|
+
episode:
|
|
81
|
+
episode_event:
|
|
@@ -92,6 +92,12 @@ class CdmTable(str, Enum):
|
|
|
92
92
|
procedure_occurrence = "procedure_occurrence"
|
|
93
93
|
condition_occurrence = "condition_occurrence"
|
|
94
94
|
device_exposure = "device_exposure"
|
|
95
|
+
visit_occurrence = "visit_occurrence"
|
|
96
|
+
death = "death"
|
|
97
|
+
specimen = "specimen"
|
|
98
|
+
fact_relationship = "fact_relationship"
|
|
99
|
+
episode = "episode"
|
|
100
|
+
episode_event = "episode_event"
|
|
95
101
|
|
|
96
102
|
|
|
97
103
|
|
|
@@ -212,6 +218,13 @@ class OmopCdmProfile(ConfiguredBaseModel):
|
|
|
212
218
|
cdm_table: CdmTable = Field(default=..., json_schema_extra = { "linkml_meta": {'domain_of': ['OmopCdmProfile']} })
|
|
213
219
|
concept_slot: str = Field(default=..., json_schema_extra = { "linkml_meta": {'domain_of': ['OmopCdmProfile']} })
|
|
214
220
|
value_slot: Optional[str] = Field(default=None, json_schema_extra = { "linkml_meta": {'domain_of': ['OmopCdmProfile']} })
|
|
221
|
+
unit_slot: Optional[str] = Field(default=None, json_schema_extra = { "linkml_meta": {'domain_of': ['OmopCdmProfile']} })
|
|
222
|
+
operator_slot: Optional[str] = Field(default=None, json_schema_extra = { "linkml_meta": {'domain_of': ['OmopCdmProfile']} })
|
|
223
|
+
modifier_slot: Optional[str] = Field(default=None, json_schema_extra = { "linkml_meta": {'domain_of': ['OmopCdmProfile']} })
|
|
224
|
+
extra_concept_slots: Optional[list[str]] = Field(default=None, json_schema_extra = { "linkml_meta": {'domain_of': ['OmopCdmProfile']} })
|
|
225
|
+
numeric_slots: Optional[list[str]] = Field(default=None, json_schema_extra = { "linkml_meta": {'domain_of': ['OmopCdmProfile']} })
|
|
226
|
+
string_slots: Optional[list[str]] = Field(default=None, json_schema_extra = { "linkml_meta": {'domain_of': ['OmopCdmProfile']} })
|
|
227
|
+
reference_slots: Optional[list[str]] = Field(default=None, json_schema_extra = { "linkml_meta": {'domain_of': ['OmopCdmProfile']} })
|
|
215
228
|
|
|
216
229
|
|
|
217
230
|
class OmopTemplate(ConfiguredBaseModel):
|
|
@@ -15,6 +15,9 @@ ObservationProfiles:
|
|
|
15
15
|
- observation_simple
|
|
16
16
|
- observation_coded
|
|
17
17
|
- observation_string
|
|
18
|
+
- observation_numeric
|
|
19
|
+
- observation_numeric_with_unit
|
|
20
|
+
- observation_with_qualifier
|
|
18
21
|
notes: Valid CDM projection profiles for the OMOP OBSERVATION table
|
|
19
22
|
|
|
20
23
|
|
|
@@ -24,7 +27,10 @@ MeasurementProfiles:
|
|
|
24
27
|
role: cdm_profile_set
|
|
25
28
|
members:
|
|
26
29
|
- measurement_numeric
|
|
30
|
+
- measurement_numeric_with_unit
|
|
31
|
+
- measurement_numeric_with_operator
|
|
27
32
|
- measurement_coded
|
|
33
|
+
- measurement_simple
|
|
28
34
|
notes: Valid CDM projection profiles for the OMOP MEASUREMENT table
|
|
29
35
|
|
|
30
36
|
|
|
@@ -34,6 +40,7 @@ ProcedureProfiles:
|
|
|
34
40
|
role: cdm_profile_set
|
|
35
41
|
members:
|
|
36
42
|
- procedure_simple
|
|
43
|
+
- procedure_with_inline_modifier
|
|
37
44
|
|
|
38
45
|
|
|
39
46
|
ConditionProfiles:
|
|
@@ -42,6 +49,7 @@ ConditionProfiles:
|
|
|
42
49
|
role: cdm_profile_set
|
|
43
50
|
members:
|
|
44
51
|
- condition_simple
|
|
52
|
+
- condition_with_status
|
|
45
53
|
|
|
46
54
|
|
|
47
55
|
DrugExposureProfiles:
|
|
@@ -50,4 +58,39 @@ DrugExposureProfiles:
|
|
|
50
58
|
role: cdm_profile_set
|
|
51
59
|
members:
|
|
52
60
|
- drug_exposure_simple
|
|
53
|
-
- drug_exposure_dose
|
|
61
|
+
- drug_exposure_dose
|
|
62
|
+
- drug_exposure_with_quantity_and_days_supply
|
|
63
|
+
- drug_exposure_with_route
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
DeviceProfiles:
|
|
67
|
+
class_uri: RegistryGroup
|
|
68
|
+
name: DeviceProfiles
|
|
69
|
+
role: cdm_profile_set
|
|
70
|
+
members:
|
|
71
|
+
- device_simple
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
VisitProfiles:
|
|
75
|
+
class_uri: RegistryGroup
|
|
76
|
+
name: VisitProfiles
|
|
77
|
+
role: cdm_profile_set
|
|
78
|
+
members:
|
|
79
|
+
- visit_simple
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
DeathProfiles:
|
|
83
|
+
class_uri: RegistryGroup
|
|
84
|
+
name: DeathProfiles
|
|
85
|
+
role: cdm_profile_set
|
|
86
|
+
members:
|
|
87
|
+
- death_simple
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
SpecimenProfiles:
|
|
91
|
+
class_uri: RegistryGroup
|
|
92
|
+
name: SpecimenProfiles
|
|
93
|
+
role: cdm_profile_set
|
|
94
|
+
members:
|
|
95
|
+
- specimen_simple
|
|
96
|
+
- specimen_with_site_status_quantity
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
profiles:
|
|
2
|
+
- name: observation_simple
|
|
3
|
+
cdm_table: observation
|
|
4
|
+
concept_slot: observation_concept_id
|
|
5
|
+
- name: observation_coded
|
|
6
|
+
cdm_table: observation
|
|
7
|
+
concept_slot: observation_concept_id
|
|
8
|
+
value_slot: value_as_concept_id
|
|
9
|
+
- name: observation_string
|
|
10
|
+
cdm_table: observation
|
|
11
|
+
concept_slot: observation_concept_id
|
|
12
|
+
value_slot: value_as_string
|
|
13
|
+
- name: observation_numeric
|
|
14
|
+
cdm_table: observation
|
|
15
|
+
concept_slot: observation_concept_id
|
|
16
|
+
value_slot: value_as_number
|
|
17
|
+
- name: observation_numeric_with_unit
|
|
18
|
+
cdm_table: observation
|
|
19
|
+
concept_slot: observation_concept_id
|
|
20
|
+
value_slot: value_as_number
|
|
21
|
+
unit_slot: unit_concept_id
|
|
22
|
+
- name: observation_with_qualifier
|
|
23
|
+
cdm_table: observation
|
|
24
|
+
concept_slot: observation_concept_id
|
|
25
|
+
extra_concept_slots:
|
|
26
|
+
- qualifier_concept_id
|
|
27
|
+
- name: measurement_numeric
|
|
28
|
+
cdm_table: measurement
|
|
29
|
+
concept_slot: measurement_concept_id
|
|
30
|
+
value_slot: value_as_number
|
|
31
|
+
- name: measurement_numeric_with_unit
|
|
32
|
+
cdm_table: measurement
|
|
33
|
+
concept_slot: measurement_concept_id
|
|
34
|
+
value_slot: value_as_number
|
|
35
|
+
unit_slot: unit_concept_id
|
|
36
|
+
- name: measurement_numeric_with_operator
|
|
37
|
+
cdm_table: measurement
|
|
38
|
+
concept_slot: measurement_concept_id
|
|
39
|
+
value_slot: value_as_number
|
|
40
|
+
unit_slot: unit_concept_id
|
|
41
|
+
operator_slot: operator_concept_id
|
|
42
|
+
- name: measurement_coded
|
|
43
|
+
cdm_table: measurement
|
|
44
|
+
concept_slot: measurement_concept_id
|
|
45
|
+
value_slot: value_as_concept_id
|
|
46
|
+
- name: measurement_simple
|
|
47
|
+
cdm_table: measurement
|
|
48
|
+
concept_slot: measurement_concept_id
|
|
49
|
+
- name: procedure_simple
|
|
50
|
+
cdm_table: procedure_occurrence
|
|
51
|
+
concept_slot: procedure_concept_id
|
|
52
|
+
- name: procedure_with_inline_modifier
|
|
53
|
+
cdm_table: procedure_occurrence
|
|
54
|
+
concept_slot: procedure_concept_id
|
|
55
|
+
modifier_slot: modifier_concept_id
|
|
56
|
+
- name: condition_simple
|
|
57
|
+
cdm_table: condition_occurrence
|
|
58
|
+
concept_slot: condition_concept_id
|
|
59
|
+
- name: condition_with_status
|
|
60
|
+
cdm_table: condition_occurrence
|
|
61
|
+
concept_slot: condition_concept_id
|
|
62
|
+
extra_concept_slots:
|
|
63
|
+
- condition_status_concept_id
|
|
64
|
+
- name: drug_exposure_simple
|
|
65
|
+
cdm_table: drug_exposure
|
|
66
|
+
concept_slot: drug_concept_id
|
|
67
|
+
- name: drug_exposure_dose
|
|
68
|
+
cdm_table: drug_exposure
|
|
69
|
+
concept_slot: drug_concept_id
|
|
70
|
+
value_slot: dose_value
|
|
71
|
+
- name: drug_exposure_with_quantity_and_days_supply
|
|
72
|
+
cdm_table: drug_exposure
|
|
73
|
+
concept_slot: drug_concept_id
|
|
74
|
+
numeric_slots:
|
|
75
|
+
- quantity
|
|
76
|
+
- days_supply
|
|
77
|
+
- name: drug_exposure_with_route
|
|
78
|
+
cdm_table: drug_exposure
|
|
79
|
+
concept_slot: drug_concept_id
|
|
80
|
+
extra_concept_slots:
|
|
81
|
+
- route_concept_id
|
|
82
|
+
- name: device_simple
|
|
83
|
+
cdm_table: device_exposure
|
|
84
|
+
concept_slot: device_concept_id
|
|
85
|
+
- name: visit_simple
|
|
86
|
+
cdm_table: visit_occurrence
|
|
87
|
+
concept_slot: visit_concept_id
|
|
88
|
+
- name: death_simple
|
|
89
|
+
cdm_table: death
|
|
90
|
+
concept_slot: death_type_concept_id
|
|
91
|
+
- name: specimen_simple
|
|
92
|
+
cdm_table: specimen
|
|
93
|
+
concept_slot: specimen_concept_id
|
|
94
|
+
- name: specimen_with_site_status_quantity
|
|
95
|
+
cdm_table: specimen
|
|
96
|
+
concept_slot: specimen_concept_id
|
|
97
|
+
extra_concept_slots:
|
|
98
|
+
- anatomic_site_concept_id
|
|
99
|
+
- disease_status_concept_id
|
|
100
|
+
numeric_slots:
|
|
101
|
+
- quantity
|
|
102
|
+
unit_slot: unit_concept_id
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
id: https://example.org/omop_semantics/cdm_profiles
|
|
2
|
-
name: cdm_profiles
|
|
3
|
-
|
|
4
|
-
imports:
|
|
5
|
-
- omop_base
|
|
6
|
-
|
|
7
|
-
classes:
|
|
8
|
-
CDMProfiles:
|
|
9
|
-
slots:
|
|
10
|
-
- profiles
|
|
11
|
-
tree_root: true
|
|
12
|
-
OmopCdmProfile:
|
|
13
|
-
slots:
|
|
14
|
-
- name
|
|
15
|
-
- cdm_table
|
|
16
|
-
- concept_slot
|
|
17
|
-
- value_slot
|
|
18
|
-
slots:
|
|
19
|
-
profiles:
|
|
20
|
-
range: OmopCdmProfile
|
|
21
|
-
multivalued: true
|
|
22
|
-
name:
|
|
23
|
-
range: string
|
|
24
|
-
required: true
|
|
25
|
-
cdm_table:
|
|
26
|
-
range: CdmTable
|
|
27
|
-
required: true
|
|
28
|
-
concept_slot:
|
|
29
|
-
range: string
|
|
30
|
-
required: true
|
|
31
|
-
value_slot:
|
|
32
|
-
range: string
|
|
33
|
-
required: false
|
|
34
|
-
enums:
|
|
35
|
-
CdmTable:
|
|
36
|
-
description: OMOP CDM table to which a semantic template applies
|
|
37
|
-
permissible_values:
|
|
38
|
-
observation:
|
|
39
|
-
measurement:
|
|
40
|
-
drug_exposure:
|
|
41
|
-
procedure_occurrence:
|
|
42
|
-
condition_occurrence:
|
|
43
|
-
device_exposure:
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
profiles:
|
|
2
|
-
- name: observation_simple
|
|
3
|
-
cdm_table: observation
|
|
4
|
-
concept_slot: observation_concept_id
|
|
5
|
-
- name: observation_coded
|
|
6
|
-
cdm_table: observation
|
|
7
|
-
concept_slot: observation_concept_id
|
|
8
|
-
value_slot: value_as_concept_id
|
|
9
|
-
- name: observation_string
|
|
10
|
-
cdm_table: observation
|
|
11
|
-
concept_slot: observation_concept_id
|
|
12
|
-
value_slot: value_as_string
|
|
13
|
-
- name: measurement_numeric
|
|
14
|
-
cdm_table: measurement
|
|
15
|
-
concept_slot: measurement_concept_id
|
|
16
|
-
value_slot: value_as_number
|
|
17
|
-
- name: measurement_coded
|
|
18
|
-
cdm_table: measurement
|
|
19
|
-
concept_slot: measurement_concept_id
|
|
20
|
-
value_slot: value_as_concept_id
|
|
21
|
-
- name: measurement_simple
|
|
22
|
-
cdm_table: measurement
|
|
23
|
-
concept_slot: measurement_concept_id
|
|
24
|
-
- name: procedure_simple
|
|
25
|
-
cdm_table: procedure_occurrence
|
|
26
|
-
concept_slot: procedure_concept_id
|
|
27
|
-
- name: condition_simple
|
|
28
|
-
cdm_table: condition_occurrence
|
|
29
|
-
concept_slot: condition_concept_id
|
|
30
|
-
- name: drug_exposure_simple
|
|
31
|
-
cdm_table: drug_exposure
|
|
32
|
-
concept_slot: drug_concept_id
|
|
33
|
-
- name: drug_exposure_dose
|
|
34
|
-
cdm_table: drug_exposure
|
|
35
|
-
concept_slot: drug_concept_id
|
|
36
|
-
value_slot: dose_value
|
|
37
|
-
- name: device_simple
|
|
38
|
-
cdm_table: device_exposure
|
|
39
|
-
concept_slot: device_concept_id
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{omop_semantics-0.2.2 → omop_semantics-0.3.0}/src/omop_semantics/runtime/default_valuesets.py
RENAMED
|
File without changes
|
|
File without changes
|
{omop_semantics-0.2.2 → omop_semantics-0.3.0}/src/omop_semantics/runtime/unknown_handlers.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
|
{omop_semantics-0.2.2 → omop_semantics-0.3.0}/src/omop_semantics/schema/generated_models/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{omop_semantics-0.2.2 → omop_semantics-0.3.0}/src/omop_semantics/schema/instances/demographic.yaml
RENAMED
|
File without changes
|
{omop_semantics-0.2.2 → omop_semantics-0.3.0}/src/omop_semantics/schema/instances/enumerators.yaml
RENAMED
|
File without changes
|
{omop_semantics-0.2.2 → omop_semantics-0.3.0}/src/omop_semantics/schema/instances/genomic.yaml
RENAMED
|
File without changes
|
|
File without changes
|
{omop_semantics-0.2.2 → omop_semantics-0.3.0}/src/omop_semantics/schema/instances/valuesets.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|