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.
Files changed (57) hide show
  1. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/PKG-INFO +7 -5
  2. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/README.md +1 -1
  3. fhirpathpy-2.2.0/fhirpathpy/__init__.py +228 -0
  4. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/engine/__init__.py +2 -1
  5. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/engine/evaluators/__init__.py +6 -4
  6. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/engine/invocations/misc.py +6 -1
  7. fhirpathpy-2.2.0/fhirpathpy/engine/invocations/navigation.py +81 -0
  8. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/engine/nodes.py +6 -3
  9. fhirpathpy-2.2.0/fhirpathpy/py.typed +0 -0
  10. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/pyproject.toml +16 -23
  11. fhirpathpy-2.0.3/fhirpathpy/__init__.py +0 -106
  12. fhirpathpy-2.0.3/fhirpathpy/engine/invocations/navigation.py +0 -55
  13. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/LICENSE.md +0 -0
  14. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/engine/invocations/__init__.py +0 -0
  15. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/engine/invocations/aggregate.py +0 -0
  16. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/engine/invocations/collections.py +0 -0
  17. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/engine/invocations/combining.py +0 -0
  18. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/engine/invocations/constants.py +0 -0
  19. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/engine/invocations/datetime.py +0 -0
  20. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/engine/invocations/equality.py +0 -0
  21. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/engine/invocations/existence.py +0 -0
  22. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/engine/invocations/filtering.py +0 -0
  23. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/engine/invocations/logic.py +0 -0
  24. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/engine/invocations/math.py +0 -0
  25. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/engine/invocations/strings.py +0 -0
  26. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/engine/invocations/subsetting.py +0 -0
  27. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/engine/invocations/types.py +0 -0
  28. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/engine/util.py +0 -0
  29. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/models/__init__.py +0 -0
  30. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/models/dstu2/choiceTypePaths.json +0 -0
  31. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/models/dstu2/path2Type.json +0 -0
  32. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/models/dstu2/pathsDefinedElsewhere.json +0 -0
  33. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/models/dstu2/type2Parent.json +0 -0
  34. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/models/r4/choiceTypePaths.json +0 -0
  35. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/models/r4/path2Type.json +0 -0
  36. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/models/r4/pathsDefinedElsewhere.json +0 -0
  37. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/models/r4/type2Parent.json +0 -0
  38. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/models/r5/choiceTypePaths.json +0 -0
  39. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/models/r5/path2Type.json +0 -0
  40. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/models/r5/pathsDefinedElsewhere.json +0 -0
  41. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/models/r5/type2Parent.json +0 -0
  42. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/models/stu3/choiceTypePaths.json +0 -0
  43. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/models/stu3/path2Type.json +0 -0
  44. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/models/stu3/pathsDefinedElsewhere.json +0 -0
  45. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/models/stu3/type2Parent.json +0 -0
  46. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/parser/ASTPathListener.py +0 -0
  47. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/parser/FHIRPath.g4 +0 -0
  48. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/parser/README.md +0 -0
  49. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/parser/__init__.py +0 -0
  50. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/parser/generated/FHIRPath.interp +0 -0
  51. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/parser/generated/FHIRPath.tokens +0 -0
  52. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/parser/generated/FHIRPathLexer.interp +0 -0
  53. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/parser/generated/FHIRPathLexer.py +0 -0
  54. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/parser/generated/FHIRPathLexer.tokens +0 -0
  55. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/parser/generated/FHIRPathListener.py +0 -0
  56. {fhirpathpy-2.0.3 → fhirpathpy-2.2.0}/fhirpathpy/parser/generated/FHIRPathParser.py +0 -0
  57. {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
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.8
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
  [![Build Status](https://github.com/beda-software/fhirpath-py/actions/workflows/build.yaml/badge.svg)](https://github.com/beda-software/fhirpath-py/actions)
36
38
  [![codecov](https://codecov.io/gh/beda-software/fhirpath-py/branch/master/graph/badge.svg)](https://codecov.io/gh/beda-software/fhirpath-py)
37
39
  [![pypi](https://img.shields.io/pypi/v/fhirpathpy.svg)](https://pypi.org/project/fhirpathpy/)
38
- [![Supported Python version](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/release/python-380/)
40
+ [![Supported Python version](https://img.shields.io/badge/python-3.10+-blue.svg)](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
  [![Build Status](https://github.com/beda-software/fhirpath-py/actions/workflows/build.yaml/badge.svg)](https://github.com/beda-software/fhirpath-py/actions)
5
5
  [![codecov](https://codecov.io/gh/beda-software/fhirpath-py/branch/master/graph/badge.svg)](https://codecov.io/gh/beda-software/fhirpath-py)
6
6
  [![pypi](https://img.shields.io/pypi/v/fhirpathpy.svg)](https://pypi.org/project/fhirpathpy/)
7
- [![Supported Python version](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/release/python-380/)
7
+ [![Supported Python version](https://img.shields.io/badge/python-3.10+-blue.svg)](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, data, tp, pr))
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
- print("TRACE:[" + label + "]", str(x))
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.8"
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 = "py39"
50
+ target-version = "py310"
64
51
  line-length = 100
65
- include = ["app/**/*.py", "tests/**/*.py"]
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
- # Black is responsible for E501
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.black", "autohooks.plugins.ruff"]
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