Coverage for src/lumen/fhirpath/__init__.py: 95%
110 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
1from __future__ import annotations
3from collections.abc import Callable, Mapping, Sequence
4from typing import Any, Protocol, TypeVar, cast
6from lumen.fhirpath.engine import do_eval
7from lumen.fhirpath.engine.invocations.constants import constants
8from lumen.fhirpath.engine.nodes import FP_Type, ResourceNode
9from lumen.fhirpath.engine.util import arraify, get_data, process_user_invocation_table, set_paths
10from lumen.fhirpath.parser import parse
12__title__ = "lumen.fhirpath"
13__version__ = "2.2.0"
14__author__ = "beda.software"
15__license__ = "MIT"
16__copyright__ = "Copyright 2026 beda.software"
18# Version synonym
19VERSION = __version__
21ResourceType = Mapping[str, Any]
22ContextType = Mapping[str, Any] | None
25def apply_parsed_path(resource, parsedPath, context=None, model=None, options=None):
26 constants.reset()
27 dataRoot = arraify(resource)
29 """
30 do_eval takes a "ctx" object, and we store things in that as we parse, so we
31 need to put user-provided variable data in a sub-object, ctx['vars'].
32 Set up default standard variables, and allow override from the variables.
33 However, we'll keep our own copy of dataRoot for internal processing.
34 """
35 vars = {"context": resource, "ucum": "http://unitsofmeasure.org"}
36 vars.update(context or {})
38 ctx = {
39 "dataRoot": dataRoot,
40 "vars": vars,
41 "model": model,
42 "userInvocationTable": process_user_invocation_table(
43 (options or {}).get("userInvocationTable", {})
44 ),
45 }
47 # Add trace callback if provided in options
48 if options and "traceFn" in options:
49 ctx["traceFn"] = options["traceFn"]
51 node = do_eval(ctx, dataRoot, parsedPath["children"][0])
53 # Resolve any internal "ResourceNode" instances. Continue to let FP_Type
54 # subclasses through.
56 if options and options.get("returnRawData", False):
57 if isinstance(node, list):
58 res = []
59 # Filter out intenal representation of primitive extensions
60 # even in this raw data mode (as they are not a part of the output)
61 for item in node:
62 if isinstance(item, ResourceNode):
63 if isinstance(item.data, dict):
64 keys = list(item.data.keys())
65 if keys == ["extension"]:
66 continue
67 res.append(item)
68 return res
69 return node
71 def visit(node):
72 data = get_data(node)
74 if isinstance(node, list):
75 res = []
76 for item in data:
77 # Filter out intenal representation of primitive extensions
78 i = visit(item)
79 if isinstance(i, dict):
80 keys = list(i.keys())
81 if keys == ["extension"]:
82 continue
83 res.append(i)
84 return res
86 if isinstance(data, dict) and not isinstance(data, FP_Type):
87 for key, value in data.items():
88 data[key] = visit(value)
90 return data
92 return visit(node)
95def evaluate(
96 resource: ResourceType | list,
97 path: str | dict,
98 context: dict | None = None,
99 model: dict | None = None,
100 options: dict | None = None,
101) -> list:
102 if model is None:
103 from lumen.fhirpath.models import models
104 model = models["r4"]
105 default_context = {
106 "sct": "http://snomed.info/sct",
107 "loinc": "http://loinc.org",
108 "ucum": "http://unitsofmeasure.org",
109 "vs-administrative-gender": "http://hl7.org/fhir/ValueSet/administrative-gender",
110 "ext-patient-birthTime": "http://hl7.org/fhir/StructureDefinition/patient-birthTime",
111 }
112 if context:
113 default_context.update(context)
115 if isinstance(path, dict):
116 node = parse(path["expression"])
117 if "base" in path:
118 resource = ResourceNode.create_node(resource, path["base"])
119 else:
120 node = parse(path)
122 return apply_parsed_path(resource, node, default_context, model, options)
125def compile(
126 path: str, model: dict | None = None, options: dict | None = None
127) -> Callable[[ResourceType, ContextType], list]:
128 if model is None:
129 from lumen.fhirpath.models import models
130 model = models["r4"]
131 return set_paths(apply_parsed_path, parsedPath=parse(path), model=model, options=options)
134InputType = TypeVar("InputType")
135OutputType = TypeVar("OutputType")
136# Contravariant: a callable accepting a wider input type is a valid subtype.
137_I_contra = TypeVar("_I_contra", contravariant=True)
138# Covariant: a callable returning a narrower output type is a valid subtype.
139# Sequence (read-only) is required here because list is invariant and rejects covariant TypeVars.
140_O_co = TypeVar("_O_co", covariant=True)
143class CompiledFirst(Protocol[_I_contra, _O_co]):
144 def __call__(self, resource: _I_contra, context: ContextType = ...) -> _O_co | None: ...
147class CompiledArray(Protocol[_I_contra, _O_co]):
148 def __call__(self, resource: _I_contra, context: ContextType = ...) -> Sequence[_O_co]: ...
151def compile_as_array(
152 expression: str, input_type: type[InputType], output_type: type[OutputType]
153) -> CompiledArray[InputType, OutputType]:
154 path_fn = compile(expression)
156 def fn(resource: Any, context: ContextType = None) -> Any:
157 return _format_result(
158 path_fn(_prepare_data(resource, input_type), context),
159 output_type,
160 is_array=True,
161 )
163 return cast(CompiledArray[InputType, OutputType], fn)
166def compile_as_first(
167 expression: str, input_type: type[InputType], output_type: type[OutputType]
168) -> CompiledFirst[InputType, OutputType]:
169 path_fn = compile(expression)
171 def fn(resource: Any, context: ContextType = None) -> Any:
172 return _format_result(
173 path_fn(_prepare_data(resource, input_type), context),
174 output_type,
175 is_array=False,
176 )
178 return cast(CompiledFirst[InputType, OutputType], fn)
181def _prepare_data(resource: Any, input_type: type[InputType]) -> ResourceType:
182 if not isinstance(resource, input_type):
183 raise Exception(
184 f"Resource type is {type(resource).__name__}, expected {input_type.__name__}"
185 )
187 if isinstance(resource, dict):
188 return resource
190 if hasattr(resource, "model_dump"):
191 return resource.model_dump()
193 raise Exception(f"Don't know how to work with type {type(resource).__name__}")
196def _format_result(result: list, output_type: type[OutputType], is_array=False) -> Any:
197 result = [_format_item(item, output_type) for item in result]
199 if is_array:
200 return result
202 if len(result) == 0:
203 return None
205 return result[0]
208def _format_item(item: Any, output_type: type[OutputType]) -> OutputType:
209 if hasattr(output_type, "model_validate"):
210 return cast(Any, output_type).model_validate(item)
212 if not isinstance(item, output_type):
213 raise Exception(
214 f"Expected result to be {output_type.__name__}, but got {type(item).__name__}"
215 )
217 return item