Coverage for src/lumen/derivation/engine.py: 98%
44 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-08-31 18:46 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-08-31 18:46 +0000
1"""
2This module applies registered derivation rules to a resource before it is
3persisted. A rule fills in ``target_path`` from Coding values found at
4``source_path`` by translating them through a ConceptMap — but only when the
5target is not already present, so client-supplied data is never overwritten.
7Best-effort by design: a missing ConceptMap, an unmapped code, or any
8evaluation problem is logged and skipped rather than failing the write. A
9tenant with no rules registered (the default — no package installs any) pays
10one empty-cache lookup and otherwise does nothing.
11"""
13from __future__ import annotations
15from typing import Any, cast
16from uuid import UUID
18import asyncpg
19import structlog
21from ..fhir.path import evaluate_path
22from ..fhir.terminology import translate
23from .registry import DerivationRuleRegistry
25logger = structlog.get_logger(__name__)
28async def apply_derivation_rules(
29 conn: asyncpg.Connection[asyncpg.Record] | asyncpg.pool.PoolConnectionProxy[asyncpg.Record],
30 tenant_id: UUID,
31 resource_type: str,
32 resource: dict[str, Any],
33 registry: DerivationRuleRegistry,
34) -> None:
35 """Mutate ``resource`` in place, filling in any registered missing fields."""
36 rules = await registry.get_rules(conn, tenant_id, resource_type)
37 if not rules:
38 return
40 for rule in rules:
41 if resource.get(rule.target_path):
42 continue # never overwrite / augment client-supplied data
44 codings = _extract_codings(evaluate_path(rule.source_path, resource))
45 if not codings:
46 continue
48 concept_map = await _find_concept_map(conn, tenant_id, rule.concept_map_url)
49 if concept_map is None:
50 await logger.awarning(
51 "derivation.concept_map_not_found",
52 resource_type=resource_type,
53 target_path=rule.target_path,
54 concept_map_url=rule.concept_map_url,
55 tenant_id=str(tenant_id),
56 )
57 continue
59 derived = _translate_first_match(concept_map, codings)
60 if derived is not None:
61 concept = {"coding": [derived]}
62 # A collection target (e.g. DocumentReference.category, 0..*) must be
63 # a JSON array of CodeableConcept; a bare object is invalid FHIR.
64 # The target was confirmed absent above, so start a fresh list.
65 resource[rule.target_path] = [concept] if rule.target_is_list else concept
66 await logger.adebug(
67 "derivation.applied",
68 resource_type=resource_type,
69 target_path=rule.target_path,
70 target_is_list=rule.target_is_list,
71 tenant_id=str(tenant_id),
72 )
75def _extract_codings(values: list[Any]) -> list[dict[str, Any]]:
76 """Keep only dict values that look like a FHIR Coding (have a ``code``)."""
77 return [v for v in values if isinstance(v, dict) and cast("dict[str, Any]", v).get("code")]
80def _translate_first_match(
81 concept_map: dict[str, Any], codings: list[dict[str, Any]]
82) -> dict[str, Any] | None:
83 for coding in codings:
84 matches = translate(concept_map, coding.get("system"), coding["code"])
85 if matches:
86 return matches[0]["concept"] # type: ignore[no-any-return]
87 return None
90async def _find_concept_map(
91 conn: asyncpg.Connection[asyncpg.Record] | asyncpg.pool.PoolConnectionProxy[asyncpg.Record],
92 tenant_id: UUID,
93 url: str,
94) -> dict[str, Any] | None:
95 row: asyncpg.Record | None = await conn.fetchrow(
96 """
97 SELECT resource
98 FROM fhir_resources
99 WHERE tenant_id = $1
100 AND resource_type = 'ConceptMap'
101 AND resource ->> 'url' = $2
102 AND deleted = FALSE
103 ORDER BY resource_id, version_id DESC
104 LIMIT 1
105 """,
106 tenant_id,
107 url,
108 )
109 if row is None:
110 return None
111 resource = row["resource"]
112 if isinstance(resource, dict):
113 return cast("dict[str, Any]", resource)
114 return None