fpml 0.0.1__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.
- fpml-0.0.1/PKG-INFO +89 -0
- fpml-0.0.1/README.md +57 -0
- fpml-0.0.1/fpml/__init__.py +12 -0
- fpml-0.0.1/fpml/core/__init__.py +0 -0
- fpml-0.0.1/fpml/core/constants.py +5 -0
- fpml-0.0.1/fpml/core/exceptions.py +11 -0
- fpml-0.0.1/fpml/core/extract.py +354 -0
- fpml-0.0.1/fpml/core/types.py +39 -0
- fpml-0.0.1/fpml/core/utils.py +17 -0
- fpml-0.0.1/pyproject.toml +79 -0
fpml-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: fpml
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: The FHIRPath mapping language is a data DSL designed to convert data from QuestionnaireResponse (and not only) to any FHIR Resource.
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: fhir,fhirpath
|
|
7
|
+
Author: Beda Software
|
|
8
|
+
Author-email: ilya@beda.software
|
|
9
|
+
Maintainer: Vadim Laletin
|
|
10
|
+
Maintainer-email: vadim@beda.software
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Environment :: Web Environment
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
|
+
Requires-Dist: fhirpathpy (>=1.2.1,<2.0.0)
|
|
26
|
+
Project-URL: Bug Tracker, https://github.com/beda-software/FHIRPathMappingLanguage/issues
|
|
27
|
+
Project-URL: Documentation, https://github.com/beda-software/FHIRPathMappingLanguage/tree/main/python/README.md
|
|
28
|
+
Project-URL: Homepage, https://github.com/beda-software/FHIRPathMappingLanguage/tree/main/python
|
|
29
|
+
Project-URL: Repository, https://github.com/beda-software/FHIRPathMappingLanguage/tree/main/python
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# FHIRPathMappingLanguage - fpml python package
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install fpml
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from fpml import resolve_template
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
resource = {
|
|
47
|
+
"resourceType": "QuestionnaireResponse",
|
|
48
|
+
"status": "completed",
|
|
49
|
+
"item": [
|
|
50
|
+
{
|
|
51
|
+
"linkId": "name",
|
|
52
|
+
"answer": [
|
|
53
|
+
{
|
|
54
|
+
"valueString": "Name"
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
template = {
|
|
62
|
+
"resourceType": "Patient",
|
|
63
|
+
"name": "{{ item.where(linkId='name').answer.valueString }}"
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
context = {}
|
|
67
|
+
|
|
68
|
+
result = resolve_template(resource, template, context)
|
|
69
|
+
|
|
70
|
+
print(result)
|
|
71
|
+
# {'resourceType': 'Patient', 'name': 'Name'}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
## Development
|
|
76
|
+
|
|
77
|
+
In `./python` directory:
|
|
78
|
+
|
|
79
|
+
Run in the shell
|
|
80
|
+
```
|
|
81
|
+
autohooks activate
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
And edit `../.git/hooks/pre-commit` by replacing the first line with
|
|
85
|
+
```
|
|
86
|
+
#!/usr/bin/env -S poetry --project=./python run python
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
|
fpml-0.0.1/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# FHIRPathMappingLanguage - fpml python package
|
|
2
|
+
|
|
3
|
+
## Installation
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install fpml
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
from fpml import resolve_template
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
resource = {
|
|
16
|
+
"resourceType": "QuestionnaireResponse",
|
|
17
|
+
"status": "completed",
|
|
18
|
+
"item": [
|
|
19
|
+
{
|
|
20
|
+
"linkId": "name",
|
|
21
|
+
"answer": [
|
|
22
|
+
{
|
|
23
|
+
"valueString": "Name"
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
template = {
|
|
31
|
+
"resourceType": "Patient",
|
|
32
|
+
"name": "{{ item.where(linkId='name').answer.valueString }}"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
context = {}
|
|
36
|
+
|
|
37
|
+
result = resolve_template(resource, template, context)
|
|
38
|
+
|
|
39
|
+
print(result)
|
|
40
|
+
# {'resourceType': 'Patient', 'name': 'Name'}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
## Development
|
|
45
|
+
|
|
46
|
+
In `./python` directory:
|
|
47
|
+
|
|
48
|
+
Run in the shell
|
|
49
|
+
```
|
|
50
|
+
autohooks activate
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
And edit `../.git/hooks/pre-commit` by replacing the first line with
|
|
54
|
+
```
|
|
55
|
+
#!/usr/bin/env -S poetry --project=./python run python
|
|
56
|
+
```
|
|
57
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import importlib.metadata
|
|
2
|
+
|
|
3
|
+
from .core.exceptions import FPMLValidationError
|
|
4
|
+
from .core.extract import resolve_template
|
|
5
|
+
|
|
6
|
+
__title__ = "fpml"
|
|
7
|
+
__version__ = importlib.metadata.version("fpml")
|
|
8
|
+
__author__ = "beda.software"
|
|
9
|
+
__license__ = "MIT"
|
|
10
|
+
__copyright__ = "Copyright 2025 beda.software"
|
|
11
|
+
|
|
12
|
+
__all__ = ["FPMLValidationError", "resolve_template"]
|
|
File without changes
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from .constants import root_node_key
|
|
2
|
+
from .types import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class FPMLValidationError(Exception):
|
|
6
|
+
def __init__(self, message: str, path: Path) -> None:
|
|
7
|
+
path_str = ".".join(str(x) for x in path if x != root_node_key)
|
|
8
|
+
super().__init__(f"{message}. Path '{path_str}'")
|
|
9
|
+
|
|
10
|
+
self.error_message = message
|
|
11
|
+
self.error_path = path_str
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Any, Optional, cast
|
|
3
|
+
|
|
4
|
+
from fhirpathpy import evaluate # type: ignore
|
|
5
|
+
|
|
6
|
+
from .constants import root_node_key, undefined
|
|
7
|
+
from .exceptions import FPMLValidationError
|
|
8
|
+
from .types import (
|
|
9
|
+
Context,
|
|
10
|
+
DictNode,
|
|
11
|
+
FPOptions,
|
|
12
|
+
Matcher,
|
|
13
|
+
MatcherResult,
|
|
14
|
+
Node,
|
|
15
|
+
Path,
|
|
16
|
+
Resource,
|
|
17
|
+
StrNode,
|
|
18
|
+
Transformer,
|
|
19
|
+
)
|
|
20
|
+
from .utils import flatten, omit_key
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def resolve_template(
|
|
24
|
+
resource: Resource,
|
|
25
|
+
template: Any,
|
|
26
|
+
context: Optional[Context] = None,
|
|
27
|
+
fp_options: Optional[FPOptions] = None,
|
|
28
|
+
strict: bool = False,
|
|
29
|
+
) -> Any:
|
|
30
|
+
assert strict is False, "strict is not supported yet"
|
|
31
|
+
|
|
32
|
+
return resolve_template_recur(
|
|
33
|
+
[],
|
|
34
|
+
resource,
|
|
35
|
+
template,
|
|
36
|
+
context or {},
|
|
37
|
+
fp_options,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def resolve_template_recur(
|
|
42
|
+
start_path: Path,
|
|
43
|
+
resource: Resource,
|
|
44
|
+
template: Any,
|
|
45
|
+
context: Context,
|
|
46
|
+
fp_options: Optional[FPOptions] = None,
|
|
47
|
+
) -> Any:
|
|
48
|
+
return iterate_node(
|
|
49
|
+
start_path,
|
|
50
|
+
{root_node_key: template},
|
|
51
|
+
context or {},
|
|
52
|
+
lambda path, node, context: process_node(path, resource, node, context, fp_options),
|
|
53
|
+
).get(root_node_key, None)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def process_node(
|
|
57
|
+
path: Path,
|
|
58
|
+
resource: Resource,
|
|
59
|
+
node: Node,
|
|
60
|
+
context: Context,
|
|
61
|
+
fp_options: Optional[FPOptions],
|
|
62
|
+
) -> tuple[Node, Context]:
|
|
63
|
+
if isinstance(node, dict):
|
|
64
|
+
new_node, new_context = process_assign_block(path, resource, node, context, fp_options)
|
|
65
|
+
|
|
66
|
+
matchers: list[Matcher] = [
|
|
67
|
+
process_context_block,
|
|
68
|
+
process_merge_block,
|
|
69
|
+
process_for_block,
|
|
70
|
+
process_if_block,
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
for matcher in matchers:
|
|
74
|
+
result = matcher(path, resource, new_node, new_context, fp_options)
|
|
75
|
+
if result:
|
|
76
|
+
return result["node"], new_context
|
|
77
|
+
|
|
78
|
+
return new_node, new_context
|
|
79
|
+
|
|
80
|
+
if isinstance(node, str):
|
|
81
|
+
return process_template_string(path, resource, node, context, fp_options), context
|
|
82
|
+
|
|
83
|
+
return node, context
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def iterate_node(start_path: Path, node: Node, context: Context, transform: Transformer) -> Node:
|
|
87
|
+
if isinstance(node, list):
|
|
88
|
+
# Arrays are flattened and null/undefined values are removed here
|
|
89
|
+
return flatten(
|
|
90
|
+
[
|
|
91
|
+
value
|
|
92
|
+
for value in [
|
|
93
|
+
iterate_node(
|
|
94
|
+
[*start_path, index],
|
|
95
|
+
*transform([*start_path, index], value, context),
|
|
96
|
+
transform,
|
|
97
|
+
)
|
|
98
|
+
for index, value in enumerate(node)
|
|
99
|
+
]
|
|
100
|
+
if value is not None and value is not undefined
|
|
101
|
+
]
|
|
102
|
+
)
|
|
103
|
+
if isinstance(node, dict):
|
|
104
|
+
# undefined values are removed from dicts, but nulls are preserved
|
|
105
|
+
return {
|
|
106
|
+
key: value
|
|
107
|
+
for key, value in {
|
|
108
|
+
key: iterate_node(
|
|
109
|
+
[*start_path, key],
|
|
110
|
+
*transform([*start_path, key], value, context),
|
|
111
|
+
transform,
|
|
112
|
+
)
|
|
113
|
+
for key, value in node.items()
|
|
114
|
+
}.items()
|
|
115
|
+
if value is not undefined
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return transform(start_path, node, context)[0]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def process_template_string(
|
|
122
|
+
path: Path,
|
|
123
|
+
resource: Resource,
|
|
124
|
+
node: StrNode,
|
|
125
|
+
context: Context,
|
|
126
|
+
fp_options: Optional[FPOptions],
|
|
127
|
+
) -> Any:
|
|
128
|
+
template_regexp = re.compile(r"{{\+?\s*([\s\S]+?)\s*\+?}}")
|
|
129
|
+
result = node
|
|
130
|
+
|
|
131
|
+
for match in template_regexp.finditer(node):
|
|
132
|
+
expr = match.group(1)
|
|
133
|
+
try:
|
|
134
|
+
replacement = evaluate_expression(path, resource, expr, context, fp_options)[0]
|
|
135
|
+
except IndexError:
|
|
136
|
+
return None if match.group(0).startswith("{{+") else undefined
|
|
137
|
+
if match.group(0) == node:
|
|
138
|
+
return replacement
|
|
139
|
+
result = result.replace(match.group(0), str(replacement))
|
|
140
|
+
|
|
141
|
+
return result
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def process_context_block(
|
|
145
|
+
path: Path,
|
|
146
|
+
resource: Resource,
|
|
147
|
+
node: DictNode,
|
|
148
|
+
context: Context,
|
|
149
|
+
fp_options: Optional[FPOptions],
|
|
150
|
+
) -> Optional[MatcherResult]:
|
|
151
|
+
keys = list(node.keys())
|
|
152
|
+
context_regexp = re.compile(r"{{\s*(.+?)\s*}}")
|
|
153
|
+
context_key = next((k for k in keys if context_regexp.match(k)), None)
|
|
154
|
+
|
|
155
|
+
if context_key:
|
|
156
|
+
matches = context_regexp.match(context_key)
|
|
157
|
+
expr = matches.group(1) if matches else ""
|
|
158
|
+
|
|
159
|
+
if len(keys) > 1:
|
|
160
|
+
raise FPMLValidationError("Context block must be presented as single key", path)
|
|
161
|
+
|
|
162
|
+
answers = evaluate_expression(path, resource, expr, context, fp_options)
|
|
163
|
+
return {
|
|
164
|
+
"node": [
|
|
165
|
+
resolve_template_recur(path, answer, node[context_key], context, fp_options)
|
|
166
|
+
for answer in answers
|
|
167
|
+
]
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def process_for_block(
|
|
174
|
+
path: Path,
|
|
175
|
+
resource: Resource,
|
|
176
|
+
node: DictNode,
|
|
177
|
+
context: Context,
|
|
178
|
+
fp_options: Optional[FPOptions],
|
|
179
|
+
) -> Optional[MatcherResult]:
|
|
180
|
+
keys = list(node.keys())
|
|
181
|
+
|
|
182
|
+
for_regexp = re.compile(r"{%\s*for\s+(?:(\w+?)\s*,\s*)?(\w+?)\s+in\s+(.+?)\s*%}")
|
|
183
|
+
for_key = next((k for k in keys if for_regexp.match(k)), None)
|
|
184
|
+
|
|
185
|
+
if for_key:
|
|
186
|
+
matches = for_regexp.match(for_key)
|
|
187
|
+
if not matches:
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
has_index_key = len(matches.groups()) == 3 # noqa: PLR2004
|
|
191
|
+
index_key = cast(str, matches.group(1)) if has_index_key else None
|
|
192
|
+
item_key = cast(str, matches.group(2) if has_index_key else matches.group(1))
|
|
193
|
+
expr = matches.group(3) if has_index_key else matches.group(2)
|
|
194
|
+
|
|
195
|
+
if len(keys) > 1:
|
|
196
|
+
raise FPMLValidationError("For block must be presented as single key", path)
|
|
197
|
+
|
|
198
|
+
answers = evaluate_expression(path, resource, expr, context, fp_options)
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
"node": [
|
|
202
|
+
resolve_template_recur(
|
|
203
|
+
path,
|
|
204
|
+
resource,
|
|
205
|
+
node[for_key],
|
|
206
|
+
{
|
|
207
|
+
**context,
|
|
208
|
+
item_key: answer,
|
|
209
|
+
**({index_key: index} if index_key else {}),
|
|
210
|
+
},
|
|
211
|
+
fp_options,
|
|
212
|
+
)
|
|
213
|
+
for index, answer in enumerate(answers)
|
|
214
|
+
]
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def process_if_block(
|
|
221
|
+
path: Path,
|
|
222
|
+
resource: Resource,
|
|
223
|
+
node: dict[str, Any],
|
|
224
|
+
context: Context,
|
|
225
|
+
fp_options: Optional[FPOptions],
|
|
226
|
+
) -> Optional[MatcherResult]:
|
|
227
|
+
keys = list(node.keys())
|
|
228
|
+
|
|
229
|
+
if_regexp = re.compile(r"{%\s*if\s+(.+?)\s*%}")
|
|
230
|
+
else_regexp = re.compile(r"{%\s*else\s*%}")
|
|
231
|
+
|
|
232
|
+
if_keys = [k for k in keys if if_regexp.match(k)]
|
|
233
|
+
if len(if_keys) > 1:
|
|
234
|
+
raise FPMLValidationError("If block must be presented once", path)
|
|
235
|
+
if_key = if_keys[0] if if_keys else None
|
|
236
|
+
|
|
237
|
+
else_keys = [k for k in keys if else_regexp.match(k)]
|
|
238
|
+
if len(else_keys) > 1:
|
|
239
|
+
raise FPMLValidationError("Else block must be presented once", path)
|
|
240
|
+
else_key = else_keys[0] if else_keys else None
|
|
241
|
+
|
|
242
|
+
if else_key and not if_key:
|
|
243
|
+
raise FPMLValidationError(
|
|
244
|
+
"Else block must be presented only when if block is presented", path
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
if not if_key:
|
|
248
|
+
return None
|
|
249
|
+
|
|
250
|
+
matches = if_regexp.match(if_key)
|
|
251
|
+
expr = matches.group(1) if matches else ""
|
|
252
|
+
|
|
253
|
+
answer = evaluate_expression(path, resource, f"iif({expr}, true, false)", context, fp_options)[
|
|
254
|
+
0
|
|
255
|
+
]
|
|
256
|
+
|
|
257
|
+
new_node = (
|
|
258
|
+
resolve_template_recur(path, resource, node[if_key], context, fp_options)
|
|
259
|
+
if answer
|
|
260
|
+
else (
|
|
261
|
+
resolve_template_recur(path, resource, node[else_key], context, fp_options)
|
|
262
|
+
if else_key
|
|
263
|
+
else undefined
|
|
264
|
+
)
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
is_merge_behavior = len(keys) != (2 if else_key else 1)
|
|
268
|
+
if is_merge_behavior:
|
|
269
|
+
if not isinstance(new_node, dict) and new_node is not None and new_node is not undefined:
|
|
270
|
+
raise FPMLValidationError(
|
|
271
|
+
"If/else block must return object for implicit merge into existing node",
|
|
272
|
+
path,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
"node": {
|
|
277
|
+
**omit_key(omit_key(node, if_key), else_key),
|
|
278
|
+
**(new_node if isinstance(new_node, dict) else {}),
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return {"node": new_node}
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def process_merge_block(
|
|
286
|
+
path: Path,
|
|
287
|
+
resource: Resource,
|
|
288
|
+
node: DictNode,
|
|
289
|
+
context: Context,
|
|
290
|
+
fp_options: Optional[FPOptions],
|
|
291
|
+
) -> Optional[MatcherResult]:
|
|
292
|
+
merge_key = next((k for k in node if re.match(r"{%\s*merge\s*%}", k)), None)
|
|
293
|
+
if merge_key:
|
|
294
|
+
merged_node = omit_key(node, merge_key)
|
|
295
|
+
values = node[merge_key] if isinstance(node[merge_key], list) else [node[merge_key]]
|
|
296
|
+
for value in values:
|
|
297
|
+
result = resolve_template_recur(path, resource, value, context, fp_options)
|
|
298
|
+
if not isinstance(result, dict) and result is not None and result is not undefined:
|
|
299
|
+
raise FPMLValidationError("Merge block must contain object", path)
|
|
300
|
+
if result is not undefined and result is not None:
|
|
301
|
+
merged_node.update(result)
|
|
302
|
+
return {"node": merged_node}
|
|
303
|
+
return None
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def process_assign_block(
|
|
307
|
+
path: Path,
|
|
308
|
+
resource: Resource,
|
|
309
|
+
node: DictNode,
|
|
310
|
+
context: Context,
|
|
311
|
+
fp_options: Optional[FPOptions],
|
|
312
|
+
) -> tuple[DictNode, Context]:
|
|
313
|
+
extended_context = context.copy()
|
|
314
|
+
assign_key = next((k for k in node if re.match(r"{%\s*assign\s*%}", k)), None)
|
|
315
|
+
if assign_key:
|
|
316
|
+
if isinstance(node[assign_key], list):
|
|
317
|
+
for obj in node[assign_key]:
|
|
318
|
+
if len(obj) != 1:
|
|
319
|
+
raise FPMLValidationError(
|
|
320
|
+
"Assign block must accept only one key per object", path
|
|
321
|
+
)
|
|
322
|
+
extended_context.update(
|
|
323
|
+
resolve_template_recur(path, resource, obj, extended_context, fp_options)
|
|
324
|
+
)
|
|
325
|
+
elif isinstance(node[assign_key], dict) and len(node[assign_key]) == 1:
|
|
326
|
+
extended_context.update(
|
|
327
|
+
resolve_template_recur(
|
|
328
|
+
path,
|
|
329
|
+
resource,
|
|
330
|
+
node[assign_key],
|
|
331
|
+
extended_context,
|
|
332
|
+
fp_options,
|
|
333
|
+
)
|
|
334
|
+
)
|
|
335
|
+
else:
|
|
336
|
+
raise FPMLValidationError("Assign block must accept array or object", path)
|
|
337
|
+
return omit_key(node, assign_key), extended_context
|
|
338
|
+
return node, context
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def evaluate_expression(
|
|
342
|
+
path: Path,
|
|
343
|
+
resource: Resource,
|
|
344
|
+
expression: str,
|
|
345
|
+
context: Context,
|
|
346
|
+
fp_options: Optional[FPOptions] = None,
|
|
347
|
+
) -> list[Any]:
|
|
348
|
+
fp_options_with_default = cast(FPOptions, fp_options or {})
|
|
349
|
+
model = fp_options_with_default.get("model")
|
|
350
|
+
|
|
351
|
+
try:
|
|
352
|
+
return evaluate(resource, expression, context, model)
|
|
353
|
+
except Exception as exc:
|
|
354
|
+
raise FPMLValidationError(f"Cannot evaluate '{expression}': {exc}", path) from exc
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# noqa: A005
|
|
2
|
+
from typing import Any, Callable, Optional, TypedDict, Union
|
|
3
|
+
|
|
4
|
+
Resource = dict[str, Any]
|
|
5
|
+
Node = Any
|
|
6
|
+
DictNode = dict[str, Any]
|
|
7
|
+
StrNode = str
|
|
8
|
+
Context = dict[str, Any]
|
|
9
|
+
|
|
10
|
+
Path = list[Union[str, int]]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Model(TypedDict):
|
|
14
|
+
choiceTypePaths: dict[str, list[str]]
|
|
15
|
+
pathsDefinedElsewhere: dict[str, str]
|
|
16
|
+
type2Parent: dict[str, str]
|
|
17
|
+
path2Type: dict[str, str]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FPOptions(TypedDict):
|
|
21
|
+
model: Optional[Model]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class MatcherResult(TypedDict):
|
|
25
|
+
node: Optional[Node]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
Matcher = Callable[
|
|
29
|
+
[
|
|
30
|
+
Path,
|
|
31
|
+
Resource,
|
|
32
|
+
DictNode,
|
|
33
|
+
Context,
|
|
34
|
+
Optional[FPOptions],
|
|
35
|
+
],
|
|
36
|
+
Optional[MatcherResult],
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
Transformer = Callable[[Path, Node, Context], tuple[Node, Context]]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from typing import Any, Optional
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def flatten(lst: list):
|
|
5
|
+
result = []
|
|
6
|
+
for item in lst:
|
|
7
|
+
if isinstance(item, list):
|
|
8
|
+
result.extend(flatten(item))
|
|
9
|
+
else:
|
|
10
|
+
result.append(item)
|
|
11
|
+
return result
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def omit_key(obj: dict[str, Any], key: Optional[str]) -> dict[str, Any]:
|
|
15
|
+
if key is None:
|
|
16
|
+
return obj
|
|
17
|
+
return {k: v for k, v in obj.items() if k != key}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "fpml"
|
|
3
|
+
description = "The FHIRPath mapping language is a data DSL designed to convert data from QuestionnaireResponse (and not only) to any FHIR Resource."
|
|
4
|
+
authors = [{ name = "Beda Software", email = "ilya@beda.software" }]
|
|
5
|
+
maintainers = [
|
|
6
|
+
{ name = "Vadim Laletin", email = "vadim@beda.software" },
|
|
7
|
+
{ name = "Ilya Beda", email = "ilya@beda.software" },
|
|
8
|
+
]
|
|
9
|
+
license = { text = "MIT" }
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
keywords = ["fhir", "fhirpath"]
|
|
12
|
+
dynamic = ["version", "classifiers"]
|
|
13
|
+
dependencies = ["fhirpathpy (>=1.2.1,<2.0.0)"]
|
|
14
|
+
|
|
15
|
+
[project.urls]
|
|
16
|
+
homepage = "https://github.com/beda-software/FHIRPathMappingLanguage/tree/main/python"
|
|
17
|
+
repository = "https://github.com/beda-software/FHIRPathMappingLanguage/tree/main/python"
|
|
18
|
+
documentation = "https://github.com/beda-software/FHIRPathMappingLanguage/tree/main/python/README.md"
|
|
19
|
+
"Bug Tracker" = "https://github.com/beda-software/FHIRPathMappingLanguage/issues"
|
|
20
|
+
|
|
21
|
+
[tool.poetry]
|
|
22
|
+
version = "0.0.1"
|
|
23
|
+
packages = [{ include = "fpml" }]
|
|
24
|
+
classifiers = [
|
|
25
|
+
"Development Status :: 5 - Production/Stable",
|
|
26
|
+
"Environment :: Web Environment",
|
|
27
|
+
"Intended Audience :: Developers",
|
|
28
|
+
"Operating System :: OS Independent",
|
|
29
|
+
"Programming Language :: Python",
|
|
30
|
+
"Programming Language :: Python :: 3",
|
|
31
|
+
"Programming Language :: Python :: 3.9",
|
|
32
|
+
"Programming Language :: Python :: 3.10",
|
|
33
|
+
"Programming Language :: Python :: 3.11",
|
|
34
|
+
"Programming Language :: Python :: 3.12",
|
|
35
|
+
"Programming Language :: Python :: 3.13",
|
|
36
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
37
|
+
]
|
|
38
|
+
readme = ["README.md"]
|
|
39
|
+
|
|
40
|
+
[tool.poetry.dependencies]
|
|
41
|
+
python = ">=3.9,<4.0"
|
|
42
|
+
|
|
43
|
+
[tool.poetry.group.dev.dependencies]
|
|
44
|
+
ruff = "^0.9.7"
|
|
45
|
+
mypy = "^1.15.0"
|
|
46
|
+
autohooks-plugin-mypy = "^23.10.0"
|
|
47
|
+
autohooks-plugin-ruff = "^25.2.0"
|
|
48
|
+
autohooks = "^25.2.0"
|
|
49
|
+
|
|
50
|
+
[tool.poetry.group.test.dependencies]
|
|
51
|
+
pytest = "^8.3.4"
|
|
52
|
+
pytest-cov = "^6.0.0"
|
|
53
|
+
pyyaml = "^6.0.2"
|
|
54
|
+
types-PyYAML = "^6.0.12"
|
|
55
|
+
|
|
56
|
+
[build-system]
|
|
57
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
58
|
+
build-backend = "poetry.core.masonry.api"
|
|
59
|
+
|
|
60
|
+
[tool.ruff]
|
|
61
|
+
target-version = "py39"
|
|
62
|
+
line-length = 100
|
|
63
|
+
|
|
64
|
+
[tool.ruff.lint]
|
|
65
|
+
select = ["I", "E", "F", "N", "B", "C4", "PT", "UP", "I001", "A", "RET", "TID251", "RUF", "SIM", "PYI", "T20", "PIE", "G", "ISC", "PL"]
|
|
66
|
+
|
|
67
|
+
[tool.mypy]
|
|
68
|
+
files = ["fpml", "tests"]
|
|
69
|
+
ignore_missing_imports = true
|
|
70
|
+
check_untyped_defs = true
|
|
71
|
+
|
|
72
|
+
[tool.autohooks]
|
|
73
|
+
mode = "poetry"
|
|
74
|
+
pre-commit = ["autohooks.plugins.mypy", "autohooks.plugins.ruff.format", "autohooks.plugins.ruff.check"]
|
|
75
|
+
|
|
76
|
+
[tool.pytest.ini_options]
|
|
77
|
+
addopts = "-ra --color=yes --cov fpml --cov-report html --doctest-modules"
|
|
78
|
+
log_cli = true
|
|
79
|
+
log_cli_level = "WARNING"
|