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.
Files changed (41) hide show
  1. omop_semantics-0.2.2/PKG-INFO +84 -0
  2. omop_semantics-0.2.2/README.md +50 -0
  3. {omop_semantics-0.2.0 → omop_semantics-0.2.2}/pyproject.toml +6 -3
  4. omop_semantics-0.2.2/src/omop_semantics/__init__.py +46 -0
  5. {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/runtime/__init__.py +4 -4
  6. {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/runtime/instance_loader.py +12 -0
  7. {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/runtime/renderers.py +2 -2
  8. {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/runtime/resolver.py +43 -25
  9. {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/runtime/value_sets.py +21 -19
  10. omop_semantics-0.2.2/src/omop_semantics/schema/codegen.py +107 -0
  11. {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/schema/configuration/core/omop_named_sets.yaml +2 -2
  12. {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/schema/configuration/core/omop_profiles.yaml +2 -1
  13. {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/schema/configuration/core/omop_templates.yaml +4 -2
  14. {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/schema/generated_models/omop_named_sets.py +9 -21
  15. {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/schema/generated_models/omop_semantic_registry.py +16 -25
  16. {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/schema/instances/profiles.yaml +7 -1
  17. {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/schema/instances/valuesets.yaml +12 -12
  18. {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/unknowns.py +4 -2
  19. omop_semantics-0.2.0/PKG-INFO +0 -138
  20. omop_semantics-0.2.0/README.md +0 -104
  21. omop_semantics-0.2.0/src/omop_semantics/__init__.py +0 -28
  22. {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/base.py +0 -0
  23. {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/runtime/default_valuesets.py +0 -0
  24. {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/runtime/unknown_handlers.py +0 -0
  25. {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/schema/__init__.py +0 -0
  26. {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/schema/configuration/core/omop_base.yaml +0 -0
  27. {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/schema/configuration/core/omop_unknowns.yaml +0 -0
  28. {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/schema/configuration/profiles/omop_episodes.yaml +0 -0
  29. {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/schema/configuration/profiles/omop_modifiers.yaml +0 -0
  30. {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/schema/configuration/profiles/omop_staging.yaml +0 -0
  31. {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/schema/configuration/registry/omop_semantic_registry.yaml +0 -0
  32. {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/schema/dump.py +0 -0
  33. {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/schema/generated_models/__init__.py +0 -0
  34. {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/schema/generated_models/template_set.py +0 -0
  35. {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/schema/instances/demographic.yaml +0 -0
  36. {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/schema/instances/enumerators.yaml +0 -0
  37. {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/schema/instances/genomic.yaml +0 -0
  38. {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/schema/instances/profile_groups.yaml +0 -0
  39. {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/schema/instances/provider_specialty.yaml +0 -0
  40. {omop_semantics-0.2.0 → omop_semantics-0.2.2}/src/omop_semantics/utils/__init__.py +0 -0
  41. {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.0"
4
- description = "Add your description here"
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>=7.0",
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
- Primary typed runtime exports for templates, profiles, and semantic inspection.
2
+ Public runtime exports for `omop_semantics`.
3
3
 
4
- The `runtime` package is the preferred entrypoint for shape-aware semantics
5
- work. The most commonly used downstream compatibility surface for stable named
6
- ids remains `omop_semantics.runtime.default_valuesets`.
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"<p><em>{h(group.notes)}</em></p>" if group.notes else
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 the resolved group hierarchies
69
- need to be resolved in downstream logic.
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 self._compiled_by_name is None:
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 intended to be the main object used by ETL pipelines, query builders,
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 and merged into a single symbolic profile
1047
- namespace for documentation and inspection.
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/symbol YAML files.
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
- try:
1067
- fragments.append(load_registry_fragment(p))
1068
- except ValidationError:
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
- return self._by_label[label]
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 AttributeError:
239
+ except KeyError:
238
240
  pass
239
241
 
240
- raise AttributeError(name)
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.members
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["members"]:
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
- members=resolved_members,
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
- - members
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
- members:
44
+ semantic_units:
45
45
  range: CDMSemanticUnits
46
46
  multivalued: true
47
47
  required: true
@@ -39,4 +39,5 @@ enums:
39
39
  measurement:
40
40
  drug_exposure:
41
41
  procedure_occurrence:
42
- condition_occurrence:
42
+ condition_occurrence:
43
+ device_exposure: