omop-semantics 0.2.0__tar.gz → 0.2.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_semantics-0.2.2/PKG-INFO +84 -0
- omop_semantics-0.2.2/README.md +50 -0
- {omop_semantics-0.2.0 → omop_semantics-0.2.2}/pyproject.toml +6 -3
- omop_semantics-0.2.2/src/omop_semantics/__init__.py +46 -0
- {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/runtime/__init__.py +4 -4
- {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/runtime/instance_loader.py +12 -0
- {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/runtime/renderers.py +2 -2
- {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/runtime/resolver.py +43 -25
- {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/runtime/value_sets.py +21 -19
- omop_semantics-0.2.2/src/omop_semantics/schema/codegen.py +107 -0
- {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/schema/configuration/core/omop_named_sets.yaml +2 -2
- {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/schema/configuration/core/omop_profiles.yaml +2 -1
- {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/schema/configuration/core/omop_templates.yaml +4 -2
- {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/schema/generated_models/omop_named_sets.py +9 -21
- {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/schema/generated_models/omop_semantic_registry.py +16 -25
- {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/schema/instances/profiles.yaml +7 -1
- {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/schema/instances/valuesets.yaml +12 -12
- {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/unknowns.py +4 -2
- omop_semantics-0.2.0/PKG-INFO +0 -138
- omop_semantics-0.2.0/README.md +0 -104
- omop_semantics-0.2.0/src/omop_semantics/__init__.py +0 -28
- {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/base.py +0 -0
- {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/runtime/default_valuesets.py +0 -0
- {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/runtime/unknown_handlers.py +0 -0
- {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/schema/__init__.py +0 -0
- {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/schema/configuration/core/omop_base.yaml +0 -0
- {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/schema/configuration/core/omop_unknowns.yaml +0 -0
- {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/schema/configuration/profiles/omop_episodes.yaml +0 -0
- {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/schema/configuration/profiles/omop_modifiers.yaml +0 -0
- {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/schema/configuration/profiles/omop_staging.yaml +0 -0
- {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/schema/configuration/registry/omop_semantic_registry.yaml +0 -0
- {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/schema/dump.py +0 -0
- {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/schema/generated_models/__init__.py +0 -0
- {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/schema/generated_models/template_set.py +0 -0
- {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/schema/instances/demographic.yaml +0 -0
- {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/schema/instances/enumerators.yaml +0 -0
- {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/schema/instances/genomic.yaml +0 -0
- {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/schema/instances/profile_groups.yaml +0 -0
- {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/schema/instances/provider_specialty.yaml +0 -0
- {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/utils/__init__.py +0 -0
- {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/utils/paths.py +0 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: omop-semantics
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Summary: Define, validate, and use schema-backed semantic conventions for OMOP CDM
|
|
5
|
+
Author: gkennos
|
|
6
|
+
Author-email: gkennos <georgina.kennedy@unsw.edu.au>
|
|
7
|
+
Requires-Dist: ipykernel>=7.2.0
|
|
8
|
+
Requires-Dist: linkml>=1.11.1
|
|
9
|
+
Requires-Dist: linkml-runtime>=1.11.1
|
|
10
|
+
Requires-Dist: python-dotenv>=1.2.2
|
|
11
|
+
Requires-Dist: ruamel-yaml>=0.18.17
|
|
12
|
+
Requires-Dist: typing-extensions>=4.15.0
|
|
13
|
+
Requires-Dist: urllib3>=2.7.0
|
|
14
|
+
Requires-Dist: ipython>=8.0 ; extra == 'dev'
|
|
15
|
+
Requires-Dist: pytest>=9.0.3 ; extra == 'dev'
|
|
16
|
+
Requires-Dist: pytest-cov>=4.0 ; extra == 'dev'
|
|
17
|
+
Requires-Dist: mypy>=1.8 ; extra == 'dev'
|
|
18
|
+
Requires-Dist: ruff>=0.4 ; extra == 'dev'
|
|
19
|
+
Requires-Dist: rich>=13.0 ; extra == 'dev'
|
|
20
|
+
Requires-Dist: tornado>=6.5.5 ; extra == 'dev'
|
|
21
|
+
Requires-Dist: pygments>=2.20.0 ; extra == 'dev'
|
|
22
|
+
Requires-Dist: mkdocs-material>=9.7.6 ; extra == 'docs'
|
|
23
|
+
Requires-Dist: mkdocstrings-python>=2.0.1 ; extra == 'docs'
|
|
24
|
+
Requires-Dist: mkdocs>=1.6.1 ; extra == 'docs'
|
|
25
|
+
Requires-Dist: mkdocs-mermaid2-plugin>=1.2.3 ; extra == 'docs'
|
|
26
|
+
Requires-Dist: pygments>=2.20.0 ; extra == 'docs'
|
|
27
|
+
Requires-Python: >=3.12
|
|
28
|
+
Project-URL: Homepage, https://australiancancerdatanetwork.github.io/omop-semantics/
|
|
29
|
+
Project-URL: Repository, https://github.com/AustralianCancerDataNetwork/omop-semantics
|
|
30
|
+
Project-URL: Issues, https://github.com/AustralianCancerDataNetwork/omop-semantics/issues
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Provides-Extra: docs
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# omop-semantics
|
|
36
|
+
|
|
37
|
+
**omop-semantics** is a Python library for defining and managing **semantic conventions on top of OMOP CDM**.
|
|
38
|
+
|
|
39
|
+
It gives you a structured, versioned, portable layer for describing which OMOP concepts are valid in a given context, how they map to CDM row shapes, and what fallback concepts to use when a mapping cannot be completed. Conventions are authored in YAML, validated against schemas, and exposed as typed Python objects — not scattered across ETL code, SQL, or documentation.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Three runtime surfaces
|
|
44
|
+
|
|
45
|
+
- **Value-set runtime**
|
|
46
|
+
Stable named concept ids for application logic:
|
|
47
|
+
`from omop_semantics.runtime.default_valuesets import runtime`
|
|
48
|
+
|
|
49
|
+
- **Template/profile runtime**
|
|
50
|
+
Compiled semantic templates and CDM profiles via `OmopSemanticEngine`.
|
|
51
|
+
|
|
52
|
+
- **Fallback concepts**
|
|
53
|
+
Canonical unknown and default concepts with reason codes:
|
|
54
|
+
`from omop_semantics.unknowns import UNKNOWN`
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Key properties
|
|
59
|
+
|
|
60
|
+
- **Portable** — no database or vocabulary graph required
|
|
61
|
+
- **Versionable** — conventions are tracked in git alongside code
|
|
62
|
+
- **Extensible** — add project-specific definitions on top of the shipped ones
|
|
63
|
+
- **Integrates with pipelines** — drives ETL logic, validation, and documentation from a single source
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## When to use this
|
|
68
|
+
|
|
69
|
+
Use **omop-semantics** if you:
|
|
70
|
+
|
|
71
|
+
- have project-specific rules about which OMOP concepts are valid,
|
|
72
|
+
- need consistent concept groupings across ETL and analytics,
|
|
73
|
+
- want semantic conventions to be explicit, testable, and versioned,
|
|
74
|
+
- are working in domains like oncology where OMOP alone is too permissive.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Docs
|
|
79
|
+
|
|
80
|
+
- [Usage](docs/usage.md) — loading paths and code patterns
|
|
81
|
+
- [Data Model](docs/data-model.md) — profiles, templates, and semantic objects
|
|
82
|
+
- [Schema & Instances](docs/schema-and-instances.md) — authoring assets and file organisation
|
|
83
|
+
- [Fallback Concepts](docs/unknowns.md) — the shipped unknown and default concepts
|
|
84
|
+
- [Internals](docs/internals.md) — package structure and load-time behaviour
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# omop-semantics
|
|
2
|
+
|
|
3
|
+
**omop-semantics** is a Python library for defining and managing **semantic conventions on top of OMOP CDM**.
|
|
4
|
+
|
|
5
|
+
It gives you a structured, versioned, portable layer for describing which OMOP concepts are valid in a given context, how they map to CDM row shapes, and what fallback concepts to use when a mapping cannot be completed. Conventions are authored in YAML, validated against schemas, and exposed as typed Python objects — not scattered across ETL code, SQL, or documentation.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Three runtime surfaces
|
|
10
|
+
|
|
11
|
+
- **Value-set runtime**
|
|
12
|
+
Stable named concept ids for application logic:
|
|
13
|
+
`from omop_semantics.runtime.default_valuesets import runtime`
|
|
14
|
+
|
|
15
|
+
- **Template/profile runtime**
|
|
16
|
+
Compiled semantic templates and CDM profiles via `OmopSemanticEngine`.
|
|
17
|
+
|
|
18
|
+
- **Fallback concepts**
|
|
19
|
+
Canonical unknown and default concepts with reason codes:
|
|
20
|
+
`from omop_semantics.unknowns import UNKNOWN`
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Key properties
|
|
25
|
+
|
|
26
|
+
- **Portable** — no database or vocabulary graph required
|
|
27
|
+
- **Versionable** — conventions are tracked in git alongside code
|
|
28
|
+
- **Extensible** — add project-specific definitions on top of the shipped ones
|
|
29
|
+
- **Integrates with pipelines** — drives ETL logic, validation, and documentation from a single source
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## When to use this
|
|
34
|
+
|
|
35
|
+
Use **omop-semantics** if you:
|
|
36
|
+
|
|
37
|
+
- have project-specific rules about which OMOP concepts are valid,
|
|
38
|
+
- need consistent concept groupings across ETL and analytics,
|
|
39
|
+
- want semantic conventions to be explicit, testable, and versioned,
|
|
40
|
+
- are working in domains like oncology where OMOP alone is too permissive.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Docs
|
|
45
|
+
|
|
46
|
+
- [Usage](docs/usage.md) — loading paths and code patterns
|
|
47
|
+
- [Data Model](docs/data-model.md) — profiles, templates, and semantic objects
|
|
48
|
+
- [Schema & Instances](docs/schema-and-instances.md) — authoring assets and file organisation
|
|
49
|
+
- [Fallback Concepts](docs/unknowns.md) — the shipped unknown and default concepts
|
|
50
|
+
- [Internals](docs/internals.md) — package structure and load-time behaviour
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "omop-semantics"
|
|
3
|
-
version = "0.2.
|
|
4
|
-
description = "
|
|
3
|
+
version = "0.2.2"
|
|
4
|
+
description = "Define, validate, and use schema-backed semantic conventions for OMOP CDM"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
7
7
|
{ name = "gkennos", email = "georgina.kennedy@unsw.edu.au" }
|
|
@@ -23,7 +23,7 @@ omop-semantics = "omop_semantics:main"
|
|
|
23
23
|
[project.optional-dependencies]
|
|
24
24
|
dev = [
|
|
25
25
|
"ipython>=8.0",
|
|
26
|
-
"pytest>=
|
|
26
|
+
"pytest>=9.0.3",
|
|
27
27
|
"pytest-cov>=4.0",
|
|
28
28
|
"mypy>=1.8",
|
|
29
29
|
"ruff>=0.4",
|
|
@@ -45,6 +45,9 @@ build-backend = "uv_build"
|
|
|
45
45
|
[tool.pytest.ini_options]
|
|
46
46
|
testpaths = ["tests"]
|
|
47
47
|
|
|
48
|
+
[tool.pyright]
|
|
49
|
+
ignore = ["src/omop_semantics/schema/generated_models"]
|
|
50
|
+
|
|
48
51
|
[project.urls]
|
|
49
52
|
Homepage = "https://australiancancerdatanetwork.github.io/omop-semantics/"
|
|
50
53
|
Repository = "https://github.com/AustralianCancerDataNetwork/omop-semantics"
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Top-level exports for `omop_semantics`.
|
|
3
|
+
|
|
4
|
+
Import from the package root for:
|
|
5
|
+
|
|
6
|
+
- fallback concepts and reason codes via ``UNKNOWN``
|
|
7
|
+
- path helpers via ``BASE_DIR``, ``SCHEMA_DIR``, and ``INSTANCE_DIR``
|
|
8
|
+
|
|
9
|
+
For runtime access, use the dedicated subpackages:
|
|
10
|
+
|
|
11
|
+
- ``from omop_semantics.runtime.default_valuesets import runtime``
|
|
12
|
+
- ``from omop_semantics.runtime import OmopSemanticEngine``
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from .unknowns import UNKNOWN, UnknownValue, UnknownReason
|
|
16
|
+
from .utils.paths import BASE_DIR, SCHEMA_DIR, INSTANCE_DIR
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
# unknowns — stable public surface
|
|
20
|
+
"UNKNOWN",
|
|
21
|
+
"UnknownValue",
|
|
22
|
+
"UnknownReason",
|
|
23
|
+
# path helpers
|
|
24
|
+
"BASE_DIR",
|
|
25
|
+
"SCHEMA_DIR",
|
|
26
|
+
"INSTANCE_DIR",
|
|
27
|
+
"main",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def main(argv: "list[str] | None" = None) -> int:
|
|
32
|
+
"""Console entry point (``omop-semantics``).
|
|
33
|
+
|
|
34
|
+
Subcommands:
|
|
35
|
+
gen-models (re)generate the committed pydantic models from the LinkML schema.
|
|
36
|
+
"""
|
|
37
|
+
import sys
|
|
38
|
+
|
|
39
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
40
|
+
if argv and argv[0] == "gen-models":
|
|
41
|
+
from omop_semantics.schema.codegen import cli as gen_models_cli
|
|
42
|
+
|
|
43
|
+
return gen_models_cli(argv[1:])
|
|
44
|
+
|
|
45
|
+
print("usage: omop-semantics gen-models [--check] [--out DIR]", file=sys.stderr)
|
|
46
|
+
return 2
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Public runtime exports for `omop_semantics`.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
Use this package when you need compiled templates, profile inspection, or
|
|
5
|
+
low-level YAML loading helpers. For the default named-id surface, import
|
|
6
|
+
``runtime`` from ``omop_semantics.runtime.default_valuesets``.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
from .instance_loader import load_registry_fragment, merge_registry_fragments, load_symbol_module
|
|
@@ -80,6 +80,18 @@ def merge_instance_files(
|
|
|
80
80
|
"groups": merged_groups
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
def needs_profile_interpolation(path: Path) -> bool:
|
|
84
|
+
"""Return True if any registry member in the file uses a string cdm_profile reference."""
|
|
85
|
+
data = yaml_loader.load_as_dict(str(path))
|
|
86
|
+
if not isinstance(data, dict):
|
|
87
|
+
return False
|
|
88
|
+
for group in data.get("groups", []) or []:
|
|
89
|
+
for member in (group.get("registry_members") or []):
|
|
90
|
+
if isinstance(member.get("cdm_profile"), str):
|
|
91
|
+
return True
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
|
|
83
95
|
def merge_registry_fragments(fragments: Sequence[RegistryFragment]) -> RegistryFragment:
|
|
84
96
|
groups: list[RegistryGroup] = []
|
|
85
97
|
|
|
@@ -243,11 +243,11 @@ def render_registry_group(group: RegistryGroup) -> Html:
|
|
|
243
243
|
],
|
|
244
244
|
)
|
|
245
245
|
|
|
246
|
+
notes = f"<p><em>{h(group.notes)}</em></p>" if group.notes else ""
|
|
246
247
|
return Html(
|
|
247
248
|
f"<h3>{h(group.name)} ({h(group.role)})</h3>"
|
|
248
249
|
f"{html}"
|
|
249
|
-
f"
|
|
250
|
-
f"<h3>{h(group.name)} ({h(group.role)})</h3>{html}"
|
|
250
|
+
f"{notes}"
|
|
251
251
|
)
|
|
252
252
|
|
|
253
253
|
def render_registry_fragment(fragment: RegistryFragment) -> Html:
|
|
@@ -2,6 +2,7 @@ from omop_semantics.schema.generated_models.omop_semantic_registry import (
|
|
|
2
2
|
OmopConcept,
|
|
3
3
|
OmopGroup,
|
|
4
4
|
OmopEnum,
|
|
5
|
+
OmopValueSet,
|
|
5
6
|
RegistryFragment,
|
|
6
7
|
RegistryGroup,
|
|
7
8
|
OmopSemanticObject,
|
|
@@ -11,7 +12,6 @@ from omop_semantics.schema.generated_models.omop_semantic_registry import (
|
|
|
11
12
|
from pathlib import Path
|
|
12
13
|
from dataclasses import dataclass
|
|
13
14
|
from typing import Iterable, TypedDict, Optional, Set
|
|
14
|
-
from pydantic import ValidationError
|
|
15
15
|
from .renderers import render_semantic_object, render_profile_object, Html, tr, h, table, as_list, render_compiled_templates
|
|
16
16
|
from .instance_loader import (
|
|
17
17
|
load_profiles,
|
|
@@ -19,6 +19,7 @@ from .instance_loader import (
|
|
|
19
19
|
merge_instance_files,
|
|
20
20
|
merge_registry_fragments,
|
|
21
21
|
load_symbol_module,
|
|
22
|
+
needs_profile_interpolation,
|
|
22
23
|
)
|
|
23
24
|
from omop_semantics.utils.paths import INSTANCE_DIR
|
|
24
25
|
|
|
@@ -64,12 +65,10 @@ class OmopSemanticResolver:
|
|
|
64
65
|
- OmopConcept resolves to its single concept_id
|
|
65
66
|
- OmopEnum resolves to the set of concept_ids of its members
|
|
66
67
|
- OmopGroup resolves to the anchor (parent) concept_ids of the group
|
|
68
|
+
- OmopValueSet resolves to the union of its members' resolved concept_ids
|
|
67
69
|
|
|
68
|
-
This library is not database-backed so
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
TODO: allow this class to extended with full resolution logic without
|
|
72
|
-
introducing dependencies on database connections or external services.
|
|
70
|
+
This library is not database-backed, so descendant expansion and vocabulary
|
|
71
|
+
graph traversal belong in downstream logic rather than in this resolver.
|
|
73
72
|
"""
|
|
74
73
|
def resolve(self, obj: OmopSemanticObject) -> set[int]:
|
|
75
74
|
"""
|
|
@@ -112,6 +111,13 @@ class OmopSemanticResolver:
|
|
|
112
111
|
if parent.concept_id is not None
|
|
113
112
|
}
|
|
114
113
|
|
|
114
|
+
if isinstance(obj, OmopValueSet):
|
|
115
|
+
# A value set composes other semantic objects; resolve to the UNION of its members.
|
|
116
|
+
ids: set[int] = set()
|
|
117
|
+
for member in (obj.members or []):
|
|
118
|
+
ids |= self.resolve(member)
|
|
119
|
+
return ids
|
|
120
|
+
|
|
115
121
|
raise TypeError(f"Unsupported semantic object: {type(obj)}")
|
|
116
122
|
|
|
117
123
|
class OmopTemplateRuntime:
|
|
@@ -362,22 +368,29 @@ class OmopRegistryRuntime:
|
|
|
362
368
|
"""
|
|
363
369
|
if self._compiled_by_name is None:
|
|
364
370
|
self.compile_index()
|
|
365
|
-
if self._compiled_by_name is None:
|
|
366
|
-
raise RuntimeError("No compiled templates available")
|
|
367
371
|
return self._compiled_by_name[name]
|
|
368
372
|
|
|
369
373
|
def get_runtime(self, name: str) -> RuntimeTemplate:
|
|
374
|
+
"""
|
|
375
|
+
Retrieve a compiled template and wrap it as a `RuntimeTemplate`.
|
|
376
|
+
|
|
377
|
+
This is the most ergonomic lookup when you want attribute access such
|
|
378
|
+
as `tpl.cdm_profile` or `tpl.entity_concept_ids`.
|
|
379
|
+
"""
|
|
370
380
|
c = self.get(name)
|
|
371
381
|
return RuntimeTemplate.from_compiled(c)
|
|
372
382
|
|
|
373
383
|
def by_role_runtime(self, role: str) -> list[RuntimeTemplate]:
|
|
384
|
+
"""Return all compiled templates for a role as `RuntimeTemplate` objects."""
|
|
374
385
|
return [RuntimeTemplate.from_compiled(c) for c in self.by_role(role)]
|
|
375
386
|
|
|
376
387
|
def allows_concept(self, template_name: str, concept_id: int) -> bool:
|
|
388
|
+
"""Return `True` if `concept_id` is allowed in the template's entity slot."""
|
|
377
389
|
tpl = self.get(template_name)
|
|
378
390
|
return concept_id in tpl["entity_concept_ids"]
|
|
379
391
|
|
|
380
392
|
def allows_value(self, template_name: str, concept_id: int) -> bool:
|
|
393
|
+
"""Return `True` if `concept_id` is allowed in the template's value slot."""
|
|
381
394
|
tpl = self.get(template_name)
|
|
382
395
|
values = tpl["value_concept_ids"]
|
|
383
396
|
return values is not None and concept_id in values
|
|
@@ -398,8 +411,6 @@ class OmopRegistryRuntime:
|
|
|
398
411
|
"""
|
|
399
412
|
if self._compiled_by_role is None:
|
|
400
413
|
self.compile_index()
|
|
401
|
-
if self._compiled_by_role is None:
|
|
402
|
-
raise RuntimeError("No compiled templates available")
|
|
403
414
|
return self._compiled_by_role.get(role, [])
|
|
404
415
|
|
|
405
416
|
def compile_all(self, role: str | None = None) -> list[CompiledTemplate]:
|
|
@@ -418,9 +429,7 @@ class OmopRegistryRuntime:
|
|
|
418
429
|
"""
|
|
419
430
|
if self._compiled_by_name is None:
|
|
420
431
|
self.compile_index()
|
|
421
|
-
if
|
|
422
|
-
raise RuntimeError("Template compilation failed")
|
|
423
|
-
if role is None or self._compiled_by_role is None:
|
|
432
|
+
if role is None:
|
|
424
433
|
return list(self._compiled_by_name.values())
|
|
425
434
|
return self._compiled_by_role.get(role, [])
|
|
426
435
|
|
|
@@ -982,8 +991,8 @@ class OmopSemanticEngine:
|
|
|
982
991
|
- the registry runtime (indexed, compiled templates),
|
|
983
992
|
- and the optional semantic profile runtime (symbolic/profile view).
|
|
984
993
|
|
|
985
|
-
It is
|
|
986
|
-
and documentation/rendering layers.
|
|
994
|
+
It is the main high-level entrypoint for shipped registry instance files,
|
|
995
|
+
ETL routing, and documentation/rendering layers.
|
|
987
996
|
"""
|
|
988
997
|
|
|
989
998
|
def __init__(
|
|
@@ -1038,20 +1047,30 @@ class OmopSemanticEngine:
|
|
|
1038
1047
|
cls,
|
|
1039
1048
|
registry_paths: Iterable[Path],
|
|
1040
1049
|
profile_paths: Iterable[Path] = (),
|
|
1050
|
+
*,
|
|
1051
|
+
profiles_path: Path | None = None,
|
|
1041
1052
|
) -> "OmopSemanticEngine":
|
|
1042
1053
|
"""
|
|
1043
1054
|
Construct a semantic engine from YAML registry and profile files.
|
|
1044
1055
|
|
|
1045
1056
|
Multiple registry fragments are merged into a single runtime registry.
|
|
1046
|
-
Profile YAML files are loaded
|
|
1047
|
-
|
|
1057
|
+
Profile YAML files are loaded into a symbolic profile namespace for
|
|
1058
|
+
documentation and inspection.
|
|
1059
|
+
|
|
1060
|
+
Registry files that store ``cdm_profile`` as a string name (e.g.
|
|
1061
|
+
``observation_simple``) are automatically expanded against the CDM
|
|
1062
|
+
profile catalogue before validation.
|
|
1048
1063
|
|
|
1049
1064
|
Parameters
|
|
1050
1065
|
----------
|
|
1051
1066
|
registry_paths
|
|
1052
1067
|
Paths to registry fragment YAML files.
|
|
1053
1068
|
profile_paths
|
|
1054
|
-
Optional paths to profile/
|
|
1069
|
+
Optional paths to symbolic profile/group YAML files that should be
|
|
1070
|
+
exposed through ``profile_runtime``.
|
|
1071
|
+
profiles_path
|
|
1072
|
+
Path to the CDM profile catalogue YAML. Defaults to the shipped
|
|
1073
|
+
``profiles.yaml`` when not provided.
|
|
1055
1074
|
|
|
1056
1075
|
Returns
|
|
1057
1076
|
-------
|
|
@@ -1060,17 +1079,16 @@ class OmopSemanticEngine:
|
|
|
1060
1079
|
"""
|
|
1061
1080
|
fragments: list[RegistryFragment] = []
|
|
1062
1081
|
profile_objects: dict[str, dict] = {}
|
|
1063
|
-
cdm_profiles = load_profiles(INSTANCE_DIR / "profiles.yaml")
|
|
1082
|
+
cdm_profiles = load_profiles(profiles_path or INSTANCE_DIR / "profiles.yaml")
|
|
1064
1083
|
|
|
1065
1084
|
for p in registry_paths:
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
# Shipped registry instance files often refer to cdm_profile by
|
|
1070
|
-
# name and need interpolation against the profile catalogue
|
|
1071
|
-
# before they conform to RegistryFragment.
|
|
1085
|
+
if needs_profile_interpolation(p):
|
|
1086
|
+
# File uses string-named cdm_profile references; expand them
|
|
1087
|
+
# against the catalogue before validating as RegistryFragment.
|
|
1072
1088
|
interpolated = merge_instance_files([p], cdm_profiles)
|
|
1073
1089
|
fragments.append(RegistryFragment.model_validate(interpolated))
|
|
1090
|
+
else:
|
|
1091
|
+
fragments.append(load_registry_fragment(p))
|
|
1074
1092
|
|
|
1075
1093
|
for p in profile_paths:
|
|
1076
1094
|
profile_objects.update(load_symbol_module(p))
|
|
@@ -1,16 +1,3 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
2
|
-
from omop_semantics.schema.generated_models.omop_named_sets import (
|
|
3
|
-
OmopConcept,
|
|
4
|
-
OmopGroup,
|
|
5
|
-
OmopEnum,
|
|
6
|
-
OmopSemanticObject,
|
|
7
|
-
CDMSemanticUnits,
|
|
8
|
-
CDMValueSet,
|
|
9
|
-
CDMValueSets
|
|
10
|
-
)
|
|
11
|
-
from abc import ABC
|
|
12
|
-
from .renderers import tr, table, h, Html
|
|
13
|
-
|
|
14
1
|
"""
|
|
15
2
|
Runtime accessors for OMOP semantic value sets.
|
|
16
3
|
|
|
@@ -40,8 +27,20 @@ as a pure read-only semantic access layer.
|
|
|
40
27
|
"""
|
|
41
28
|
|
|
42
29
|
from abc import ABC
|
|
30
|
+
from dataclasses import dataclass
|
|
43
31
|
from typing import Mapping
|
|
44
32
|
|
|
33
|
+
from omop_semantics.schema.generated_models.omop_named_sets import (
|
|
34
|
+
OmopConcept,
|
|
35
|
+
OmopGroup,
|
|
36
|
+
OmopEnum,
|
|
37
|
+
OmopSemanticObject,
|
|
38
|
+
CDMSemanticUnits,
|
|
39
|
+
CDMValueSet,
|
|
40
|
+
CDMValueSets,
|
|
41
|
+
)
|
|
42
|
+
from .renderers import tr, table, h, Html
|
|
43
|
+
|
|
45
44
|
class _RuntimeLabelledConcepts(ABC):
|
|
46
45
|
"""
|
|
47
46
|
Thin shared base for runtime objects that expose a label -> concept_id mapping.
|
|
@@ -67,7 +66,10 @@ class _RuntimeLabelledConcepts(ABC):
|
|
|
67
66
|
def __getattr__(self, label: str) -> int:
|
|
68
67
|
if label.startswith("_"):
|
|
69
68
|
raise AttributeError(label)
|
|
70
|
-
|
|
69
|
+
try:
|
|
70
|
+
return self._by_label[label]
|
|
71
|
+
except KeyError:
|
|
72
|
+
raise AttributeError(label) from None
|
|
71
73
|
|
|
72
74
|
def __repr__(self) -> str:
|
|
73
75
|
labels = ", ".join(self.labels)
|
|
@@ -234,10 +236,10 @@ class RuntimeSemanticUnit:
|
|
|
234
236
|
for value in labelled_item.values():
|
|
235
237
|
try:
|
|
236
238
|
return getattr(value, name)
|
|
237
|
-
except
|
|
239
|
+
except KeyError:
|
|
238
240
|
pass
|
|
239
241
|
|
|
240
|
-
raise
|
|
242
|
+
raise KeyError(name)
|
|
241
243
|
|
|
242
244
|
def __repr__(self) -> str:
|
|
243
245
|
parts = []
|
|
@@ -404,7 +406,7 @@ def compile_valuesets(defs: CDMValueSets) -> RuntimeValueSets:
|
|
|
404
406
|
for vs in defs.valuesets:
|
|
405
407
|
members = {
|
|
406
408
|
(unit.name or "[unlabelled]"): RuntimeSemanticUnit(unit)
|
|
407
|
-
for unit in vs.
|
|
409
|
+
for unit in vs.semantic_units
|
|
408
410
|
}
|
|
409
411
|
|
|
410
412
|
compiled[vs.valueset_name] = RuntimeValueSet(
|
|
@@ -485,7 +487,7 @@ def interpolate_valuesets(
|
|
|
485
487
|
for vs in raw["valuesets"]:
|
|
486
488
|
resolved_members: list[CDMSemanticUnits] = []
|
|
487
489
|
|
|
488
|
-
for name in vs["
|
|
490
|
+
for name in vs["semantic_units"]:
|
|
489
491
|
if name not in semantic_index:
|
|
490
492
|
raise KeyError(f"Unknown semantic unit referenced in valuesets.yaml: {name}")
|
|
491
493
|
|
|
@@ -516,7 +518,7 @@ def interpolate_valuesets(
|
|
|
516
518
|
valuesets.append(
|
|
517
519
|
CDMValueSet(
|
|
518
520
|
valueset_name=vs["name"],
|
|
519
|
-
|
|
521
|
+
semantic_units=resolved_members,
|
|
520
522
|
)
|
|
521
523
|
)
|
|
522
524
|
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""In-code generation of the committed pydantic models from the LinkML schema.
|
|
2
|
+
|
|
3
|
+
Replaces the removed ``runtime/utils.py::generate_pydantic_from_linkml`` (which shelled out to
|
|
4
|
+
``uv run gen-pydantic``). Uses the LinkML :class:`PydanticGenerator` API directly, so the generation
|
|
5
|
+
options live in code (single source of truth) and the output is reproducible against the pinned
|
|
6
|
+
linkml version - no remembered CLI flags.
|
|
7
|
+
|
|
8
|
+
CLI (wired via ``omop-semantics`` console script)::
|
|
9
|
+
|
|
10
|
+
omop-semantics gen-models # (re)write the committed modules
|
|
11
|
+
omop-semantics gen-models --check # exit non-zero if committed models are stale
|
|
12
|
+
omop-semantics gen-models --out DIR # write elsewhere (e.g. a temp dir)
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import os
|
|
18
|
+
import sys
|
|
19
|
+
from contextlib import contextmanager
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from linkml.generators.pydanticgen import PydanticGenerator
|
|
23
|
+
|
|
24
|
+
from omop_semantics.utils.paths import BASE_DIR
|
|
25
|
+
|
|
26
|
+
# Repo root (.../omop-semantics). Generation runs with cwd here and passes repo-root-relative schema
|
|
27
|
+
# paths, so the ``source_file`` stamp baked into each module is portable (not machine-specific).
|
|
28
|
+
REPO_ROOT = BASE_DIR.parent.parent
|
|
29
|
+
GENERATED_DIR = BASE_DIR / "schema" / "generated_models"
|
|
30
|
+
|
|
31
|
+
# Empty-list-for-multivalued behaviour.
|
|
32
|
+
# False -> linkml >=1.11 default: optional multivalued slots default to ``None``.
|
|
33
|
+
# True -> pre-1.11 style: default ``[]`` plus a ``treat_empty_lists_as_none`` serializer.
|
|
34
|
+
# The runtime reads these collections defensively (``x or []``), so ``None`` is safe; we adopt the
|
|
35
|
+
# current toolchain default. Change in ONE place if the project ever wants the old style back.
|
|
36
|
+
EMPTY_LIST_FOR_MULTIVALUED = False
|
|
37
|
+
|
|
38
|
+
# Generated module -> source schema, as paths RELATIVE TO REPO_ROOT.
|
|
39
|
+
# ``omop_semantic_registry`` is the primary runtime module: its schema imports the full tree
|
|
40
|
+
# (registry -> core/omop_templates -> omop_base + omop_profiles), so one regen covers all of them.
|
|
41
|
+
# ``template_set`` is intentionally excluded: it is imported nowhere (vestigial).
|
|
42
|
+
GEN_TARGETS: dict[str, str] = {
|
|
43
|
+
"omop_semantic_registry": "src/omop_semantics/schema/configuration/registry/omop_semantic_registry.yaml",
|
|
44
|
+
"omop_named_sets": "src/omop_semantics/schema/configuration/core/omop_named_sets.yaml",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@contextmanager
|
|
49
|
+
def _chdir(path: Path):
|
|
50
|
+
prev = Path.cwd()
|
|
51
|
+
os.chdir(path)
|
|
52
|
+
try:
|
|
53
|
+
yield
|
|
54
|
+
finally:
|
|
55
|
+
os.chdir(prev)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def render_module(schema_rel: str) -> str:
|
|
59
|
+
"""Return the generated pydantic module source for one schema (path relative to REPO_ROOT)."""
|
|
60
|
+
with _chdir(REPO_ROOT):
|
|
61
|
+
return PydanticGenerator(
|
|
62
|
+
schema_rel,
|
|
63
|
+
empty_list_for_multivalued_slots=EMPTY_LIST_FOR_MULTIVALUED,
|
|
64
|
+
).serialize()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def generate_models(out_dir: Path | None = None) -> list[Path]:
|
|
68
|
+
"""Generate every committed pydantic module. Returns the written paths."""
|
|
69
|
+
target_dir = out_dir or GENERATED_DIR
|
|
70
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
71
|
+
written: list[Path] = []
|
|
72
|
+
for module, schema_rel in GEN_TARGETS.items():
|
|
73
|
+
path = target_dir / f"{module}.py"
|
|
74
|
+
path.write_text(render_module(schema_rel))
|
|
75
|
+
written.append(path)
|
|
76
|
+
return written
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def check_models() -> list[str]:
|
|
80
|
+
"""Return the names of any committed modules that differ from a fresh generation."""
|
|
81
|
+
return [
|
|
82
|
+
module
|
|
83
|
+
for module, schema_rel in GEN_TARGETS.items()
|
|
84
|
+
if (GENERATED_DIR / f"{module}.py").read_text() != render_module(schema_rel)
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def cli(argv: list[str] | None = None) -> int:
|
|
89
|
+
parser = argparse.ArgumentParser(
|
|
90
|
+
prog="omop-semantics gen-models",
|
|
91
|
+
description="Generate the committed pydantic models from the LinkML schema.",
|
|
92
|
+
)
|
|
93
|
+
parser.add_argument("--out", type=Path, default=None,
|
|
94
|
+
help="Output directory (default: the committed generated_models dir).")
|
|
95
|
+
parser.add_argument("--check", action="store_true",
|
|
96
|
+
help="Do not write; exit 1 if committed models differ from a fresh generation.")
|
|
97
|
+
args = parser.parse_args(argv)
|
|
98
|
+
|
|
99
|
+
if args.check:
|
|
100
|
+
drift = check_models()
|
|
101
|
+
for module in drift:
|
|
102
|
+
print(f"DRIFT: {module}.py is out of sync - run `omop-semantics gen-models`", file=sys.stderr)
|
|
103
|
+
return 1 if drift else 0
|
|
104
|
+
|
|
105
|
+
for path in generate_models(args.out):
|
|
106
|
+
print(f"wrote {path}")
|
|
107
|
+
return 0
|
|
@@ -12,7 +12,7 @@ classes:
|
|
|
12
12
|
CDMValueSet:
|
|
13
13
|
slots:
|
|
14
14
|
- valueset_name
|
|
15
|
-
-
|
|
15
|
+
- semantic_units
|
|
16
16
|
CDMSemanticUnits:
|
|
17
17
|
slots:
|
|
18
18
|
- name
|
|
@@ -41,7 +41,7 @@ slots:
|
|
|
41
41
|
required: true
|
|
42
42
|
name:
|
|
43
43
|
range: string
|
|
44
|
-
|
|
44
|
+
semantic_units:
|
|
45
45
|
range: CDMSemanticUnits
|
|
46
46
|
multivalued: true
|
|
47
47
|
required: true
|