fhirpathpy 2.0.3__tar.gz → 2.2.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.
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/PKG-INFO +7 -5
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/README.md +1 -1
- fhirpathpy-2.2.0/fhirpathpy/__init__.py +228 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/engine/__init__.py +2 -1
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/engine/evaluators/__init__.py +6 -4
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/engine/invocations/misc.py +6 -1
- fhirpathpy-2.2.0/fhirpathpy/engine/invocations/navigation.py +81 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/engine/nodes.py +6 -3
- fhirpathpy-2.2.0/fhirpathpy/py.typed +0 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/pyproject.toml +16 -23
- fhirpathpy-2.0.3/fhirpathpy/__init__.py +0 -106
- fhirpathpy-2.0.3/fhirpathpy/engine/invocations/navigation.py +0 -55
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/LICENSE.md +0 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/engine/invocations/__init__.py +0 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/engine/invocations/aggregate.py +0 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/engine/invocations/collections.py +0 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/engine/invocations/combining.py +0 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/engine/invocations/constants.py +0 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/engine/invocations/datetime.py +0 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/engine/invocations/equality.py +0 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/engine/invocations/existence.py +0 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/engine/invocations/filtering.py +0 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/engine/invocations/logic.py +0 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/engine/invocations/math.py +0 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/engine/invocations/strings.py +0 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/engine/invocations/subsetting.py +0 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/engine/invocations/types.py +0 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/engine/util.py +0 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/models/__init__.py +0 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/models/dstu2/choiceTypePaths.json +0 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/models/dstu2/path2Type.json +0 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/models/dstu2/pathsDefinedElsewhere.json +0 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/models/dstu2/type2Parent.json +0 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/models/r4/choiceTypePaths.json +0 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/models/r4/path2Type.json +0 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/models/r4/pathsDefinedElsewhere.json +0 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/models/r4/type2Parent.json +0 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/models/r5/choiceTypePaths.json +0 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/models/r5/path2Type.json +0 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/models/r5/pathsDefinedElsewhere.json +0 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/models/r5/type2Parent.json +0 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/models/stu3/choiceTypePaths.json +0 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/models/stu3/path2Type.json +0 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/models/stu3/pathsDefinedElsewhere.json +0 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/models/stu3/type2Parent.json +0 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/parser/ASTPathListener.py +0 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/parser/FHIRPath.g4 +0 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/parser/README.md +0 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/parser/__init__.py +0 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/parser/generated/FHIRPath.interp +0 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/parser/generated/FHIRPath.tokens +0 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/parser/generated/FHIRPathLexer.interp +0 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/parser/generated/FHIRPathLexer.py +0 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/parser/generated/FHIRPathLexer.tokens +0 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/parser/generated/FHIRPathListener.py +0 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/parser/generated/FHIRPathParser.py +0 -0
- {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/parser/generated/__init__.py +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fhirpathpy
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: FHIRPath implementation in Python
|
|
5
5
|
Keywords: fhir,fhirpath
|
|
6
6
|
Author-email: "beda.software" <fhirpath@beda.software>
|
|
7
|
-
Requires-Python: >=3.
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
9
|
Classifier: Development Status :: 5 - Production/Stable
|
|
10
10
|
Classifier: Environment :: Web Environment
|
|
@@ -12,17 +12,19 @@ Classifier: Intended Audience :: Developers
|
|
|
12
12
|
Classifier: Operating System :: OS Independent
|
|
13
13
|
Classifier: Programming Language :: Python
|
|
14
14
|
Classifier: Programming Language :: Python :: 3
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
17
15
|
Classifier: Programming Language :: Python :: 3.10
|
|
18
16
|
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
19
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
20
20
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Typing :: Typed
|
|
21
22
|
License-File: LICENSE.md
|
|
22
23
|
Requires-Dist: antlr4-python3-runtime~=4.10
|
|
23
24
|
Requires-Dist: python-dateutil~=2.8
|
|
24
25
|
Requires-Dist: pytest==7.1.1 ; extra == "test"
|
|
25
26
|
Requires-Dist: pyyaml==5.4 ; extra == "test"
|
|
27
|
+
Requires-Dist: pydantic==2.13.1 ; extra == "test"
|
|
26
28
|
Project-URL: Changelog, https://github.com/beda-software/fhirpath-py/blob/master/CHANGELOG.md
|
|
27
29
|
Project-URL: Documentation, https://github.com/beda-software/fhirpath-py#readme
|
|
28
30
|
Project-URL: Homepage, https://github.com/beda-software/fhirpath-py
|
|
@@ -35,7 +37,7 @@ fhirpath.py
|
|
|
35
37
|
[](https://github.com/beda-software/fhirpath-py/actions)
|
|
36
38
|
[](https://codecov.io/gh/beda-software/fhirpath-py)
|
|
37
39
|
[](https://pypi.org/project/fhirpathpy/)
|
|
38
|
-
[](https://www.python.org/downloads/release/python-3100/)
|
|
39
41
|
|
|
40
42
|
[FHIRPath](https://www.hl7.org/fhir/fhirpath.html) implementation in Python
|
|
41
43
|
|
|
@@ -4,7 +4,7 @@ fhirpath.py
|
|
|
4
4
|
[](https://github.com/beda-software/fhirpath-py/actions)
|
|
5
5
|
[](https://codecov.io/gh/beda-software/fhirpath-py)
|
|
6
6
|
[](https://pypi.org/project/fhirpathpy/)
|
|
7
|
-
[](https://www.python.org/downloads/release/python-3100/)
|
|
8
8
|
|
|
9
9
|
[FHIRPath](https://www.hl7.org/fhir/fhirpath.html) implementation in Python
|
|
10
10
|
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
4
|
+
from typing import Any, Protocol, TypeVar, cast
|
|
5
|
+
|
|
6
|
+
from fhirpathpy.engine import do_eval
|
|
7
|
+
from fhirpathpy.engine.invocations.constants import constants
|
|
8
|
+
from fhirpathpy.engine.nodes import FP_Type, ResourceNode
|
|
9
|
+
from fhirpathpy.engine.util import arraify, get_data, process_user_invocation_table, set_paths
|
|
10
|
+
from fhirpathpy.parser import parse
|
|
11
|
+
|
|
12
|
+
__title__ = "fhirpathpy"
|
|
13
|
+
__version__ = "2.2.0"
|
|
14
|
+
__author__ = "beda.software"
|
|
15
|
+
__license__ = "MIT"
|
|
16
|
+
__copyright__ = "Copyright 2026 beda.software"
|
|
17
|
+
|
|
18
|
+
# Version synonym
|
|
19
|
+
VERSION = __version__
|
|
20
|
+
|
|
21
|
+
ResourceType = Mapping[str, Any]
|
|
22
|
+
ContextType = Mapping[str, Any] | None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def apply_parsed_path(resource, parsedPath, context=None, model=None, options=None):
|
|
26
|
+
constants.reset()
|
|
27
|
+
dataRoot = arraify(resource)
|
|
28
|
+
|
|
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 {})
|
|
37
|
+
|
|
38
|
+
ctx = {
|
|
39
|
+
"dataRoot": dataRoot,
|
|
40
|
+
"vars": vars,
|
|
41
|
+
"model": model,
|
|
42
|
+
"userInvocationTable": process_user_invocation_table(
|
|
43
|
+
(options or {}).get("userInvocationTable", {})
|
|
44
|
+
),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Add trace callback if provided in options
|
|
48
|
+
if options and "traceFn" in options:
|
|
49
|
+
ctx["traceFn"] = options["traceFn"]
|
|
50
|
+
|
|
51
|
+
node = do_eval(ctx, dataRoot, parsedPath["children"][0])
|
|
52
|
+
|
|
53
|
+
# Resolve any internal "ResourceNode" instances. Continue to let FP_Type
|
|
54
|
+
# subclasses through.
|
|
55
|
+
|
|
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
|
|
70
|
+
|
|
71
|
+
def visit(node):
|
|
72
|
+
data = get_data(node)
|
|
73
|
+
|
|
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
|
|
85
|
+
|
|
86
|
+
if isinstance(data, dict) and not isinstance(data, FP_Type):
|
|
87
|
+
for key, value in data.items():
|
|
88
|
+
data[key] = visit(value)
|
|
89
|
+
|
|
90
|
+
return data
|
|
91
|
+
|
|
92
|
+
return visit(node)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def 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
|
+
"""
|
|
103
|
+
Evaluates the "path" FHIRPath expression on the given resource, using data
|
|
104
|
+
from "context" for variables mentioned in the "path" expression.
|
|
105
|
+
|
|
106
|
+
Parameters:
|
|
107
|
+
resource (dict|list): FHIR resource, bundle as js object or array of resources This resource will be modified by this function to add type information.
|
|
108
|
+
path (string): fhirpath expression, sample 'Patient.name.given'
|
|
109
|
+
context (dict): a hash of variable name/value pairs.
|
|
110
|
+
model (dict): The "model" data object specific to a domain, e.g. R4.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
int: Description of return value
|
|
114
|
+
|
|
115
|
+
"""
|
|
116
|
+
if isinstance(path, dict):
|
|
117
|
+
node = parse(path["expression"])
|
|
118
|
+
if "base" in path:
|
|
119
|
+
resource = ResourceNode.create_node(resource, path["base"])
|
|
120
|
+
else:
|
|
121
|
+
node = parse(path)
|
|
122
|
+
|
|
123
|
+
return apply_parsed_path(resource, node, context or {}, model, options)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def compile(
|
|
127
|
+
path: str, model: dict | None = None, options: dict | None = None
|
|
128
|
+
) -> Callable[[ResourceType, ContextType], list]:
|
|
129
|
+
"""
|
|
130
|
+
Returns a function that takes a resource and an optional context hash (see
|
|
131
|
+
"evaluate"), and returns the result of evaluating the given FHIRPath
|
|
132
|
+
expression on that resource. The advantage of this function over "evaluate"
|
|
133
|
+
is that if you have multiple resources, the given FHIRPath expression will
|
|
134
|
+
only be parsed once.
|
|
135
|
+
|
|
136
|
+
Parameters:
|
|
137
|
+
path (string) - the FHIRPath expression to be parsed.
|
|
138
|
+
model (dict) - The "model" data object specific to a domain, e.g. R4.
|
|
139
|
+
|
|
140
|
+
For example, you could pass in the result of require("fhirpath/fhir-context/r4")
|
|
141
|
+
"""
|
|
142
|
+
return set_paths(apply_parsed_path, parsedPath=parse(path), model=model, options=options)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
InputType = TypeVar("InputType")
|
|
146
|
+
OutputType = TypeVar("OutputType")
|
|
147
|
+
# Contravariant: a callable accepting a wider input type is a valid subtype.
|
|
148
|
+
_I_contra = TypeVar("_I_contra", contravariant=True)
|
|
149
|
+
# Covariant: a callable returning a narrower output type is a valid subtype.
|
|
150
|
+
# Sequence (read-only) is required here because list is invariant and rejects covariant TypeVars.
|
|
151
|
+
_O_co = TypeVar("_O_co", covariant=True)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class CompiledFirst(Protocol[_I_contra, _O_co]):
|
|
155
|
+
def __call__(self, resource: _I_contra, context: ContextType = ...) -> _O_co | None: ...
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class CompiledArray(Protocol[_I_contra, _O_co]):
|
|
159
|
+
def __call__(self, resource: _I_contra, context: ContextType = ...) -> Sequence[_O_co]: ...
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def compile_as_array(
|
|
163
|
+
expression: str, input_type: type[InputType], output_type: type[OutputType]
|
|
164
|
+
) -> CompiledArray[InputType, OutputType]:
|
|
165
|
+
path_fn = compile(expression)
|
|
166
|
+
|
|
167
|
+
def fn(resource: Any, context: ContextType = None) -> Any:
|
|
168
|
+
return _format_result(
|
|
169
|
+
path_fn(_prepare_data(resource, input_type), context),
|
|
170
|
+
output_type,
|
|
171
|
+
is_array=True,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
return cast(CompiledArray[InputType, OutputType], fn)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def compile_as_first(
|
|
178
|
+
expression: str, input_type: type[InputType], output_type: type[OutputType]
|
|
179
|
+
) -> CompiledFirst[InputType, OutputType]:
|
|
180
|
+
path_fn = compile(expression)
|
|
181
|
+
|
|
182
|
+
def fn(resource: Any, context: ContextType = None) -> Any:
|
|
183
|
+
return _format_result(
|
|
184
|
+
path_fn(_prepare_data(resource, input_type), context),
|
|
185
|
+
output_type,
|
|
186
|
+
is_array=False,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
return cast(CompiledFirst[InputType, OutputType], fn)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _prepare_data(resource: Any, input_type: type[InputType]) -> ResourceType:
|
|
193
|
+
if not isinstance(resource, input_type):
|
|
194
|
+
raise Exception(
|
|
195
|
+
f"Resource type is {type(resource).__name__}, expected {input_type.__name__}"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
if isinstance(resource, dict):
|
|
199
|
+
return resource
|
|
200
|
+
|
|
201
|
+
if hasattr(resource, "model_dump"):
|
|
202
|
+
return resource.model_dump()
|
|
203
|
+
|
|
204
|
+
raise Exception(f"Don't know how to work with type {type(resource).__name__}")
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _format_result(result: list, output_type: type[OutputType], is_array=False) -> Any:
|
|
208
|
+
result = [_format_item(item, output_type) for item in result]
|
|
209
|
+
|
|
210
|
+
if is_array:
|
|
211
|
+
return result
|
|
212
|
+
|
|
213
|
+
if len(result) == 0:
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
return result[0]
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _format_item(item: Any, output_type: type[OutputType]) -> OutputType:
|
|
220
|
+
if hasattr(output_type, "model_validate"):
|
|
221
|
+
return cast(Any, output_type).model_validate(item)
|
|
222
|
+
|
|
223
|
+
if not isinstance(item, output_type):
|
|
224
|
+
raise Exception(
|
|
225
|
+
f"Expected result to be {output_type.__name__}, but got {type(item).__name__}"
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
return item
|
|
@@ -83,10 +83,11 @@ def doInvoke(ctx, fn_name, data, raw_params):
|
|
|
83
83
|
params = []
|
|
84
84
|
argTypes = invocation["arity"][paramsNumber]
|
|
85
85
|
|
|
86
|
+
thisValue = ctx["$this"] if "$this" in ctx else ctx["dataRoot"]
|
|
86
87
|
for i in range(0, paramsNumber):
|
|
87
88
|
tp = argTypes[i]
|
|
88
89
|
pr = raw_params[i]
|
|
89
|
-
params.append(make_param(ctx,
|
|
90
|
+
params.append(make_param(ctx, thisValue, tp, pr))
|
|
90
91
|
|
|
91
92
|
params.insert(0, data)
|
|
92
93
|
params.insert(0, ctx)
|
|
@@ -174,6 +174,8 @@ def create_reduce_member_invocation(model, key):
|
|
|
174
174
|
def func(acc, res):
|
|
175
175
|
res = nodes.ResourceNode.create_node(res)
|
|
176
176
|
childPath = f"{res.path}.{key}" if res.path else f"_.{key}"
|
|
177
|
+
fullPath = f"{res.propName}.{key}" if res.propName else childPath # The full path to the node (weill evenutally be) e.g. Patient.name[0].given
|
|
178
|
+
fullPath = fullPath.replace("_", "")
|
|
177
179
|
|
|
178
180
|
actualTypes = None
|
|
179
181
|
toAdd = None
|
|
@@ -212,16 +214,16 @@ def create_reduce_member_invocation(model, key):
|
|
|
212
214
|
|
|
213
215
|
if util.is_some(toAdd):
|
|
214
216
|
if isinstance(toAdd, list):
|
|
215
|
-
mapped = [nodes.ResourceNode.create_node(x, childPath) for x in toAdd]
|
|
217
|
+
mapped = [nodes.ResourceNode.create_node(x, childPath, propName=f"{fullPath}[{i}]", index=i) for i, x in enumerate(toAdd)]
|
|
216
218
|
acc = acc + mapped
|
|
217
219
|
else:
|
|
218
|
-
acc.append(nodes.ResourceNode.create_node(toAdd, childPath))
|
|
220
|
+
acc.append(nodes.ResourceNode.create_node(toAdd, childPath, propName=fullPath))
|
|
219
221
|
if util.is_some(toAdd_):
|
|
220
222
|
if isinstance(toAdd_, list):
|
|
221
|
-
mapped = [nodes.ResourceNode.create_node(x, childPath) for x in toAdd_]
|
|
223
|
+
mapped = [nodes.ResourceNode.create_node(x, childPath, propName=f"{fullPath}[{i}]", index=i) for i, x in enumerate(toAdd_)]
|
|
222
224
|
acc = acc + mapped
|
|
223
225
|
else:
|
|
224
|
-
acc.append(nodes.ResourceNode.create_node(toAdd_, childPath))
|
|
226
|
+
acc.append(nodes.ResourceNode.create_node(toAdd_, childPath, propName=fullPath))
|
|
225
227
|
return acc
|
|
226
228
|
|
|
227
229
|
return func
|
|
@@ -21,7 +21,12 @@ def iif_macro(ctx, data, cond, ok, fail=None):
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
def trace_fn(ctx, x, label=""):
|
|
24
|
-
|
|
24
|
+
# Check if a custom trace callback is provided in the context
|
|
25
|
+
if "traceFn" in ctx and callable(ctx["traceFn"]):
|
|
26
|
+
ctx["traceFn"](label, x)
|
|
27
|
+
else:
|
|
28
|
+
# Fall back to console output if no callback is provided
|
|
29
|
+
print("TRACE:[" + label + "]", str(x))
|
|
25
30
|
return x
|
|
26
31
|
|
|
27
32
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from collections import abc
|
|
2
|
+
from functools import reduce
|
|
3
|
+
import fhirpathpy.engine.util as util
|
|
4
|
+
import fhirpathpy.engine.nodes as nodes
|
|
5
|
+
|
|
6
|
+
create_node = nodes.ResourceNode.create_node
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def create_reduce_children(ctx, exclude_primitive_extensions):
|
|
10
|
+
model = ctx["model"]
|
|
11
|
+
|
|
12
|
+
def func(acc, res):
|
|
13
|
+
data = util.get_data(res)
|
|
14
|
+
res = create_node(res)
|
|
15
|
+
|
|
16
|
+
if isinstance(data, list):
|
|
17
|
+
data = dict((i, data[i]) for i in range(0, len(data)))
|
|
18
|
+
|
|
19
|
+
if isinstance(data, abc.Mapping):
|
|
20
|
+
for prop in data.keys():
|
|
21
|
+
value = data[prop]
|
|
22
|
+
childPath = ""
|
|
23
|
+
|
|
24
|
+
# extensions shouldn't filter through here, yet they should for descendants?
|
|
25
|
+
# unless this item is the node that is being processed (primitive extension)
|
|
26
|
+
# though if you filter it, descendants will not work too
|
|
27
|
+
if prop.startswith("_") and exclude_primitive_extensions:
|
|
28
|
+
continue
|
|
29
|
+
|
|
30
|
+
if res.path is not None:
|
|
31
|
+
childPath = res.path + "." + prop
|
|
32
|
+
|
|
33
|
+
fullPath = f"{res.propName}.{prop}" if res.propName else childPath # The full path to the node (weill evenutally be) e.g. Patient.name[0].given
|
|
34
|
+
fullPath = fullPath.replace("_", "")
|
|
35
|
+
|
|
36
|
+
if prop == "extension":
|
|
37
|
+
childPath = "Extension"
|
|
38
|
+
|
|
39
|
+
if (
|
|
40
|
+
isinstance(model, dict)
|
|
41
|
+
and "pathsDefinedElsewhere" in model
|
|
42
|
+
and childPath in model["pathsDefinedElsewhere"]
|
|
43
|
+
):
|
|
44
|
+
childPath = model["pathsDefinedElsewhere"][childPath]
|
|
45
|
+
|
|
46
|
+
childPath = (
|
|
47
|
+
model["path2Type"].get(childPath, childPath)
|
|
48
|
+
if isinstance(model, dict) and "path2Type" in model
|
|
49
|
+
else childPath
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# If the prop tolower ends with the type tolower
|
|
53
|
+
if prop.lower().endswith(childPath.lower()) and len(prop) > len(childPath):
|
|
54
|
+
# Check if the path is actually in the choice types
|
|
55
|
+
altPropName = res.path + "." + prop[:-len(childPath)]
|
|
56
|
+
actualTypes = model["choiceTypePaths"].get(altPropName, [])
|
|
57
|
+
if len(actualTypes) > 0:
|
|
58
|
+
# If it is, we can use it
|
|
59
|
+
fullPath = f"{res.propName}.{prop[:-len(childPath)]}"
|
|
60
|
+
|
|
61
|
+
if isinstance(value, list):
|
|
62
|
+
mapped = [create_node(n, childPath, propName=f"{fullPath}[{i}]", index=i) for i, n in enumerate(value)]
|
|
63
|
+
acc = acc + mapped
|
|
64
|
+
else:
|
|
65
|
+
acc.append(create_node(value, childPath, propName=fullPath))
|
|
66
|
+
return acc
|
|
67
|
+
|
|
68
|
+
return func
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def children(ctx, coll):
|
|
72
|
+
return reduce(create_reduce_children(ctx, True), coll, [])
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def descendants(ctx, coll):
|
|
76
|
+
res = []
|
|
77
|
+
ch = reduce(create_reduce_children(ctx, False), coll, [])
|
|
78
|
+
while len(ch) > 0:
|
|
79
|
+
res = res + ch
|
|
80
|
+
ch = reduce(create_reduce_children(ctx, False), ch, [])
|
|
81
|
+
return res
|
|
@@ -8,6 +8,7 @@ import math
|
|
|
8
8
|
import json
|
|
9
9
|
import re
|
|
10
10
|
import time
|
|
11
|
+
from typing import Optional
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
timeRE = (
|
|
@@ -821,7 +822,7 @@ class ResourceNode:
|
|
|
821
822
|
* @param _data additional data stored in a property named with "_" prepended.
|
|
822
823
|
"""
|
|
823
824
|
|
|
824
|
-
def __init__(self, data, path, _data=None):
|
|
825
|
+
def __init__(self, data, path, _data=None, propName=None, index=None):
|
|
825
826
|
"""
|
|
826
827
|
If data is a resource (maybe a contained resource) reset the path
|
|
827
828
|
information to the resource type.
|
|
@@ -832,6 +833,8 @@ class ResourceNode:
|
|
|
832
833
|
self.path = path
|
|
833
834
|
self.data = data
|
|
834
835
|
self._data = _data
|
|
836
|
+
self.propName: Optional[str] = propName
|
|
837
|
+
self.index: Optional[int] = index
|
|
835
838
|
|
|
836
839
|
def __eq__(self, value):
|
|
837
840
|
if isinstance(value, ResourceNode):
|
|
@@ -864,10 +867,10 @@ class ResourceNode:
|
|
|
864
867
|
return json.dumps(self.data)
|
|
865
868
|
|
|
866
869
|
@staticmethod
|
|
867
|
-
def create_node(data, path=None, _data=None):
|
|
870
|
+
def create_node(data, path=None, _data=None, propName=None, index=None):
|
|
868
871
|
if isinstance(data, ResourceNode):
|
|
869
872
|
return data
|
|
870
|
-
return ResourceNode(data, path, _data)
|
|
873
|
+
return ResourceNode(data, path, _data, propName, index)
|
|
871
874
|
|
|
872
875
|
def convert_data(self):
|
|
873
876
|
data = self.data
|
|
File without changes
|
|
@@ -1,17 +1,3 @@
|
|
|
1
|
-
[tool.black]
|
|
2
|
-
line-length = 100
|
|
3
|
-
target-version = ['py311']
|
|
4
|
-
exclude = '''
|
|
5
|
-
(
|
|
6
|
-
/(
|
|
7
|
-
| \.git
|
|
8
|
-
| \.pytest_cache
|
|
9
|
-
| pyproject.toml
|
|
10
|
-
| dist
|
|
11
|
-
)/
|
|
12
|
-
)
|
|
13
|
-
'''
|
|
14
|
-
|
|
15
1
|
[tool.pytest.ini_options]
|
|
16
2
|
minversion = "6.0"
|
|
17
3
|
addopts = "-ra -q --color=yes --cov=fhirpathpy --cov-report=xml"
|
|
@@ -40,17 +26,18 @@ classifiers = [
|
|
|
40
26
|
"Operating System :: OS Independent",
|
|
41
27
|
"Programming Language :: Python",
|
|
42
28
|
"Programming Language :: Python :: 3",
|
|
43
|
-
"Programming Language :: Python :: 3.8",
|
|
44
|
-
"Programming Language :: Python :: 3.9",
|
|
45
29
|
"Programming Language :: Python :: 3.10",
|
|
46
30
|
"Programming Language :: Python :: 3.11",
|
|
31
|
+
"Programming Language :: Python :: 3.12",
|
|
32
|
+
"Programming Language :: Python :: 3.13",
|
|
47
33
|
"Topic :: Internet :: WWW/HTTP",
|
|
48
34
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
35
|
+
"Typing :: Typed",
|
|
49
36
|
]
|
|
50
|
-
requires-python = ">=3.
|
|
37
|
+
requires-python = ">=3.10"
|
|
51
38
|
|
|
52
39
|
[project.optional-dependencies]
|
|
53
|
-
test = ["pytest==7.1.1", "pyyaml==5.4"]
|
|
40
|
+
test = ["pytest==7.1.1", "pyyaml==5.4", "pydantic==2.13.1"]
|
|
54
41
|
|
|
55
42
|
[project.urls]
|
|
56
43
|
Homepage = "https://github.com/beda-software/fhirpath-py"
|
|
@@ -60,16 +47,22 @@ Changelog = "https://github.com/beda-software/fhirpath-py/blob/master/CHANGELOG.
|
|
|
60
47
|
|
|
61
48
|
|
|
62
49
|
[tool.ruff]
|
|
63
|
-
target-version = "
|
|
50
|
+
target-version = "py310"
|
|
64
51
|
line-length = 100
|
|
65
|
-
include = ["
|
|
52
|
+
include = ["fhirpathpy/**/*.py", "tests/**/*.py"]
|
|
66
53
|
|
|
67
54
|
[tool.ruff.lint]
|
|
68
55
|
select = ["B", "F", "I", "E", "UP", "N", "PL", "PERF"]
|
|
69
|
-
#
|
|
70
|
-
ignore = ["E501"]
|
|
56
|
+
# N803/N806 is not relevant for us because we use camelCase for historical reasons
|
|
57
|
+
ignore = ["E501", "N803", "N806"]
|
|
71
58
|
unfixable = ["F401"]
|
|
72
59
|
|
|
60
|
+
[tool.mypy]
|
|
61
|
+
python_version = "3.10"
|
|
62
|
+
ignore_missing_imports = true
|
|
63
|
+
plugins = ["pydantic.mypy"]
|
|
64
|
+
|
|
65
|
+
|
|
73
66
|
[tool.autohooks]
|
|
74
67
|
mode = "pipenv"
|
|
75
|
-
pre-commit = ["autohooks.plugins.
|
|
68
|
+
pre-commit = ["autohooks.plugins.ruff.check", "autohooks.plugins.ruff.format"]
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
from fhirpathpy.engine.invocations.constants import constants
|
|
2
|
-
from fhirpathpy.parser import parse
|
|
3
|
-
from fhirpathpy.engine import do_eval
|
|
4
|
-
from fhirpathpy.engine.util import arraify, get_data, set_paths, process_user_invocation_table
|
|
5
|
-
from fhirpathpy.engine.nodes import FP_Type, ResourceNode
|
|
6
|
-
|
|
7
|
-
__title__ = "fhirpathpy"
|
|
8
|
-
__version__ = "2.0.3"
|
|
9
|
-
__author__ = "beda.software"
|
|
10
|
-
__license__ = "MIT"
|
|
11
|
-
__copyright__ = "Copyright 2025 beda.software"
|
|
12
|
-
|
|
13
|
-
# Version synonym
|
|
14
|
-
VERSION = __version__
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def apply_parsed_path(resource, parsedPath, context=None, model=None, options=None):
|
|
18
|
-
constants.reset()
|
|
19
|
-
dataRoot = arraify(resource)
|
|
20
|
-
|
|
21
|
-
"""
|
|
22
|
-
do_eval takes a "ctx" object, and we store things in that as we parse, so we
|
|
23
|
-
need to put user-provided variable data in a sub-object, ctx['vars'].
|
|
24
|
-
Set up default standard variables, and allow override from the variables.
|
|
25
|
-
However, we'll keep our own copy of dataRoot for internal processing.
|
|
26
|
-
"""
|
|
27
|
-
vars = {"context": resource, "ucum": "http://unitsofmeasure.org"}
|
|
28
|
-
vars.update(context or {})
|
|
29
|
-
|
|
30
|
-
ctx = {
|
|
31
|
-
"dataRoot": dataRoot,
|
|
32
|
-
"vars": vars,
|
|
33
|
-
"model": model,
|
|
34
|
-
"userInvocationTable": process_user_invocation_table(
|
|
35
|
-
(options or {}).get("userInvocationTable", {})
|
|
36
|
-
),
|
|
37
|
-
}
|
|
38
|
-
node = do_eval(ctx, dataRoot, parsedPath["children"][0])
|
|
39
|
-
|
|
40
|
-
# Resolve any internal "ResourceNode" instances. Continue to let FP_Type
|
|
41
|
-
# subclasses through.
|
|
42
|
-
|
|
43
|
-
def visit(node):
|
|
44
|
-
data = get_data(node)
|
|
45
|
-
|
|
46
|
-
if isinstance(node, list):
|
|
47
|
-
res = []
|
|
48
|
-
for item in data:
|
|
49
|
-
# Filter out intenal representation of primitive extensions
|
|
50
|
-
i = visit(item)
|
|
51
|
-
if isinstance(i, dict):
|
|
52
|
-
keys = list(i.keys())
|
|
53
|
-
if keys == ["extension"]:
|
|
54
|
-
continue
|
|
55
|
-
res.append(i)
|
|
56
|
-
return res
|
|
57
|
-
|
|
58
|
-
if isinstance(data, dict) and not isinstance(data, FP_Type):
|
|
59
|
-
for key, value in data.items():
|
|
60
|
-
data[key] = visit(value)
|
|
61
|
-
|
|
62
|
-
return data
|
|
63
|
-
|
|
64
|
-
return visit(node)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def evaluate(resource, path, context=None, model=None, options=None):
|
|
68
|
-
"""
|
|
69
|
-
Evaluates the "path" FHIRPath expression on the given resource, using data
|
|
70
|
-
from "context" for variables mentioned in the "path" expression.
|
|
71
|
-
|
|
72
|
-
Parameters:
|
|
73
|
-
resource (dict|list): FHIR resource, bundle as js object or array of resources This resource will be modified by this function to add type information.
|
|
74
|
-
path (string): fhirpath expression, sample 'Patient.name.given'
|
|
75
|
-
context (dict): a hash of variable name/value pairs.
|
|
76
|
-
model (dict): The "model" data object specific to a domain, e.g. R4.
|
|
77
|
-
|
|
78
|
-
Returns:
|
|
79
|
-
int: Description of return value
|
|
80
|
-
|
|
81
|
-
"""
|
|
82
|
-
if isinstance(path, dict):
|
|
83
|
-
node = parse(path["expression"])
|
|
84
|
-
if "base" in path:
|
|
85
|
-
resource = ResourceNode.create_node(resource, path["base"])
|
|
86
|
-
else:
|
|
87
|
-
node = parse(path)
|
|
88
|
-
|
|
89
|
-
return apply_parsed_path(resource, node, context or {}, model, options)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def compile(path, model=None, options=None):
|
|
93
|
-
"""
|
|
94
|
-
Returns a function that takes a resource and an optional context hash (see
|
|
95
|
-
"evaluate"), and returns the result of evaluating the given FHIRPath
|
|
96
|
-
expression on that resource. The advantage of this function over "evaluate"
|
|
97
|
-
is that if you have multiple resources, the given FHIRPath expression will
|
|
98
|
-
only be parsed once.
|
|
99
|
-
|
|
100
|
-
Parameters:
|
|
101
|
-
path (string) - the FHIRPath expression to be parsed.
|
|
102
|
-
model (dict) - The "model" data object specific to a domain, e.g. R4.
|
|
103
|
-
|
|
104
|
-
For example, you could pass in the result of require("fhirpath/fhir-context/r4")
|
|
105
|
-
"""
|
|
106
|
-
return set_paths(apply_parsed_path, parsedPath=parse(path), model=model, options=options)
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
from collections import abc
|
|
2
|
-
from functools import reduce
|
|
3
|
-
import fhirpathpy.engine.util as util
|
|
4
|
-
import fhirpathpy.engine.nodes as nodes
|
|
5
|
-
|
|
6
|
-
create_node = nodes.ResourceNode.create_node
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def create_reduce_children(ctx):
|
|
10
|
-
model = ctx["model"]
|
|
11
|
-
|
|
12
|
-
def func(acc, res):
|
|
13
|
-
data = util.get_data(res)
|
|
14
|
-
res = create_node(res)
|
|
15
|
-
|
|
16
|
-
if isinstance(data, list):
|
|
17
|
-
data = dict((i, data[i]) for i in range(0, len(data)))
|
|
18
|
-
|
|
19
|
-
if isinstance(data, abc.Mapping):
|
|
20
|
-
for prop in data.keys():
|
|
21
|
-
value = data[prop]
|
|
22
|
-
childPath = ""
|
|
23
|
-
|
|
24
|
-
if res.path is not None:
|
|
25
|
-
childPath = res.path + "." + prop
|
|
26
|
-
|
|
27
|
-
if (
|
|
28
|
-
isinstance(model, dict)
|
|
29
|
-
and "pathsDefinedElsewhere" in model
|
|
30
|
-
and childPath in model["pathsDefinedElsewhere"]
|
|
31
|
-
):
|
|
32
|
-
childPath = model["pathsDefinedElsewhere"][childPath]
|
|
33
|
-
|
|
34
|
-
if isinstance(value, list):
|
|
35
|
-
mapped = [create_node(n, childPath) for n in value]
|
|
36
|
-
acc = acc + mapped
|
|
37
|
-
else:
|
|
38
|
-
acc.append(create_node(value, childPath))
|
|
39
|
-
return acc
|
|
40
|
-
|
|
41
|
-
return func
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def children(ctx, coll):
|
|
45
|
-
return reduce(create_reduce_children(ctx), coll, [])
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def descendants(ctx, coll):
|
|
49
|
-
res = []
|
|
50
|
-
ch = children(ctx, coll)
|
|
51
|
-
while len(ch) > 0:
|
|
52
|
-
res = res + ch
|
|
53
|
-
ch = children(ctx, ch)
|
|
54
|
-
|
|
55
|
-
return res
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|