fhir-sync 0.10.3__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.
- fhir_sync-0.10.3/PKG-INFO +541 -0
- fhir_sync-0.10.3/README.md +527 -0
- fhir_sync-0.10.3/fhir_sync.egg-info/PKG-INFO +541 -0
- fhir_sync-0.10.3/fhir_sync.egg-info/SOURCES.txt +14 -0
- fhir_sync-0.10.3/fhir_sync.egg-info/dependency_links.txt +1 -0
- fhir_sync-0.10.3/fhir_sync.egg-info/entry_points.txt +2 -0
- fhir_sync-0.10.3/fhir_sync.egg-info/requires.txt +8 -0
- fhir_sync-0.10.3/fhir_sync.egg-info/top_level.txt +1 -0
- fhir_sync-0.10.3/fhirsync/__init__.py +15 -0
- fhir_sync-0.10.3/fhirsync/bundler.py +642 -0
- fhir_sync-0.10.3/fhirsync/cli.py +48 -0
- fhir_sync-0.10.3/fhirsync/config.py +504 -0
- fhir_sync-0.10.3/fhirsync/rules.py +571 -0
- fhir_sync-0.10.3/fhirsync/sources.py +162 -0
- fhir_sync-0.10.3/pyproject.toml +40 -0
- fhir_sync-0.10.3/setup.cfg +4 -0
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fhir-sync
|
|
3
|
+
Version: 0.10.3
|
|
4
|
+
Summary: FHIR resource bundler and synchronization library
|
|
5
|
+
Requires-Python: >=3.13
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: fhir.resources>=8.0.0
|
|
8
|
+
Requires-Dist: dic-helper-toolkit>=1.0.4
|
|
9
|
+
Requires-Dist: fhirpathpy>=2.0
|
|
10
|
+
Requires-Dist: pydantic>=2.0
|
|
11
|
+
Requires-Dist: pyyaml>=6.0
|
|
12
|
+
Provides-Extra: xml
|
|
13
|
+
Requires-Dist: dic-helper-toolkit[xml]>=1.0.4; extra == "xml"
|
|
14
|
+
|
|
15
|
+
# fhirsync
|
|
16
|
+
|
|
17
|
+
fhirsync copies FHIR resources from a source FHIR server or from FHIR files to a target FHIR server or to files. It
|
|
18
|
+
searches the source server (or reads the files), follows the references of every result and puts each result together
|
|
19
|
+
with the resources it references into a FHIR Bundle, so every Bundle can be written to the target on its own. On the
|
|
20
|
+
way, configurable **processing rules** filter and change the resources, for example to point references to the
|
|
21
|
+
target's patients or to give resources stable identifiers.
|
|
22
|
+
|
|
23
|
+
fhirsync is a command line tool and a Python library. Its main use today is syncing patient consents from **gICS**
|
|
24
|
+
(the consent management of the THS Greifswald) to a FHIR server: on demand with the `fhirsync` command, and
|
|
25
|
+
event-driven with the separate project **gics-consent-sync**, which listens to gICS's MQTT notifications and runs
|
|
26
|
+
fhirsync as a library. Nothing in fhirsync is specific to gICS, though: what happens to the resources is entirely
|
|
27
|
+
defined by the configuration.
|
|
28
|
+
|
|
29
|
+
- [How it works](#how-it-works)
|
|
30
|
+
- [Requirements](#requirements)
|
|
31
|
+
- [Installation](#installation)
|
|
32
|
+
- [Usage](#usage)
|
|
33
|
+
- [Configuration](#configuration)
|
|
34
|
+
- [Processing rules](#processing-rules)
|
|
35
|
+
- [Using fhirsync as a library](#using-fhirsync-as-a-library)
|
|
36
|
+
- [Important behaviour](#important-behaviour)
|
|
37
|
+
- [Syncing gICS consents](#syncing-gics-consents)
|
|
38
|
+
- [Upgrading from fhirsync 0.8 and 0.9](#upgrading-from-fhirsync-08-and-09)
|
|
39
|
+
- [Upgrading from fhirsync 0.7 and older](#upgrading-from-fhirsync-07-and-older)
|
|
40
|
+
- [Testing](#testing)
|
|
41
|
+
- [Project structure](#project-structure), and [ARCHITECTURE.md](ARCHITECTURE.md) for developers
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
## How it works
|
|
45
|
+
|
|
46
|
+
```mermaid
|
|
47
|
+
flowchart LR
|
|
48
|
+
source[("source: FHIR server<br/>e.g. gICS TTP-FHIR Gateway,<br/>or FHIR files")]
|
|
49
|
+
subgraph fhirsync
|
|
50
|
+
direction TB
|
|
51
|
+
filter["1. filter rules"] --> references["2. follow references<br/>(recursive)"]
|
|
52
|
+
references --> transform["3. transform rules"]
|
|
53
|
+
transform --> request["4. build Bundle entry requests"]
|
|
54
|
+
request --> finalize["5. finalize rules"]
|
|
55
|
+
end
|
|
56
|
+
target[("target<br/>FHIR server or JSON files")]
|
|
57
|
+
|
|
58
|
+
source -- "search results<br/>or the resources of the files" --> filter
|
|
59
|
+
references -- "read referenced resources" --> source
|
|
60
|
+
finalize -- "transaction Bundles<br/>PUT / POST" --> target
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
For every result (a search result, or a resource from the files) fhirsync:
|
|
64
|
+
|
|
65
|
+
1. **Filters** it: rules can skip resources or read values from them.
|
|
66
|
+
2. **Follows its references**: resources referenced as `Type/id` are read from the source and processed the same way,
|
|
67
|
+
recursively. Every Reference counts, wherever it is in the resource, also in lists like `Observation.performer`.
|
|
68
|
+
References to other resources in the Bundle are rewritten to the conditional form
|
|
69
|
+
`Type?identifier=system|value`, so the target links them to its own copies.
|
|
70
|
+
3. **Transforms** it: rules set and remove elements and identifiers.
|
|
71
|
+
4. **Builds the request** of its Bundle entry, by default a conditional update `PUT Type?identifier=system|value`.
|
|
72
|
+
5. **Finalizes** it: rules remove what only the request needed, e.g. the source server's `id`.
|
|
73
|
+
|
|
74
|
+
A Bundle holds a configurable number of results with their referenced resources. It is posted to the target server,
|
|
75
|
+
or written to a file.
|
|
76
|
+
|
|
77
|
+
The source is one of two types:
|
|
78
|
+
|
|
79
|
+
- **`server`**: a FHIR server. fhirsync runs the searches configured in `source.searches`, or the search given with
|
|
80
|
+
`-q`, one after another, and syncs their results.
|
|
81
|
+
- **`files`**: FHIR files in a directory (JSON, NDJSON or FHIR XML, optionally gzip compressed). All files are read,
|
|
82
|
+
every resource in them is a result (or only those of `source.resource_types`), and references are resolved from the
|
|
83
|
+
files.
|
|
84
|
+
|
|
85
|
+
The `fhirsync` command does one such run, for initial loads, re-syncs, imports or tests. Applications run the same
|
|
86
|
+
from their own code (see [Using fhirsync as a library](#using-fhirsync-as-a-library)).
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
## Requirements
|
|
90
|
+
|
|
91
|
+
- **Python 3.13 or newer** and [uv](https://docs.astral.sh/uv/).
|
|
92
|
+
- **A source FHIR server** reachable from the host, or **FHIR files**. For gICS, see
|
|
93
|
+
[Syncing gICS consents](#syncing-gics-consents).
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
## Installation
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
git clone <repository url> fhirsync
|
|
100
|
+
cd fhirsync
|
|
101
|
+
uv sync
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
All dependencies come from PyPI, including fhirsync's HTTP and file helpers
|
|
105
|
+
[`dic-helper-toolkit`](https://pypi.org/project/dic-helper-toolkit/). fhirsync itself is published on PyPI as
|
|
106
|
+
[`fhir-sync`](https://pypi.org/project/fhir-sync/); applications add it as a dependency (see
|
|
107
|
+
[Using fhirsync as a library](#using-fhirsync-as-a-library)).
|
|
108
|
+
|
|
109
|
+
`uv sync` also installs the development tools (`pytest`, `responses`) from the `dev` dependency group. For a
|
|
110
|
+
production installation without them, use `uv sync --no-dev`.
|
|
111
|
+
|
|
112
|
+
Reading FHIR XML files needs the extra `xml` (it installs `lxml` through `dic-helper-toolkit[xml]`):
|
|
113
|
+
`uv sync --extra xml`, or `fhir-sync[xml]` as a dependency.
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
## Usage
|
|
117
|
+
|
|
118
|
+
```sh
|
|
119
|
+
uv run fhirsync [-c CONFIG] [-q QUERY]
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
| Option | Meaning |
|
|
123
|
+
|---|---|
|
|
124
|
+
| `-c`, `--config` | the configuration file, default: `config.yaml` in the current directory |
|
|
125
|
+
| `-q`, `--query` | a FHIR search instead of `source.searches`, e.g. `Consent?domain:identifier=MII`; source type `server` only |
|
|
126
|
+
|
|
127
|
+
### Searches (source type server)
|
|
128
|
+
|
|
129
|
+
Without `-q`, fhirsync runs the searches of `source.searches`, one after another:
|
|
130
|
+
|
|
131
|
+
```yaml
|
|
132
|
+
source:
|
|
133
|
+
base_url: https://gicsserver/ttp-fhir/fhir/gics/
|
|
134
|
+
# what applies to every search of a type
|
|
135
|
+
request_params:
|
|
136
|
+
Consent:
|
|
137
|
+
category: http://fhir.de/ConsentManagement/CodeSystem/ResultType|consent-status
|
|
138
|
+
# what to fetch: only what is specific to each search
|
|
139
|
+
searches:
|
|
140
|
+
- Consent?domain:identifier=MII
|
|
141
|
+
- ResearchStudy
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`-q` replaces them for one run, e.g. for a re-sync or a single patient:
|
|
145
|
+
|
|
146
|
+
```sh
|
|
147
|
+
# all consent-status Consents of the MII domain (the category comes from source.request_params)
|
|
148
|
+
uv run fhirsync -c config.yaml -q 'Consent?domain:identifier=MII'
|
|
149
|
+
|
|
150
|
+
# all resources of a type, with the parameters configured for it
|
|
151
|
+
uv run fhirsync -c config.yaml -q Consent
|
|
152
|
+
|
|
153
|
+
# a single patient's consent status
|
|
154
|
+
uv run fhirsync -c config.yaml -q 'Consent?domain:identifier=MII&patient.identifier=https://ths-greifswald.de/fhir/gics/identifiers/Pseudonym|1000000001'
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
- **A search** is `<ResourceType>` or `<ResourceType>?<parameters>`, relative to `source.base_url`. Quote `-q` in the
|
|
158
|
+
shell, because of `&` and `|`. `-q` takes one search.
|
|
159
|
+
- **`searches` and `request_params` have different roles.** A search says *what* is fetched: put only what is
|
|
160
|
+
specific to it into the search, e.g. a domain or a patient. `source.request_params` says *how* every search of a
|
|
161
|
+
resource type is made: put what applies to all of them there, e.g. `_count`, `_profile` or a `category`. They are
|
|
162
|
+
added to every search of that type: the `searches`, `-q`, searches of applications such as gics-consent-sync, and
|
|
163
|
+
searches for `Type?…` references. A parameter given in both places takes the search's value, except for `Type?…`
|
|
164
|
+
references, where the configured value wins.
|
|
165
|
+
- **Paging:** fhirsync pages through all results, following `next` links, or using offsets if `source.next_param` is
|
|
166
|
+
set.
|
|
167
|
+
- **Without `-q` and without `source.searches`** there is nothing to run, which is an invalid command line.
|
|
168
|
+
|
|
169
|
+
### Files (source type files)
|
|
170
|
+
|
|
171
|
+
```yaml
|
|
172
|
+
source:
|
|
173
|
+
type: files
|
|
174
|
+
directory: /data/export
|
|
175
|
+
recursive: true
|
|
176
|
+
resource_types: [Consent]
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
```sh
|
|
180
|
+
uv run fhirsync -c config.yaml
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
- **All files are read first**, when fhirsync starts: the files in `directory` (and its subdirectories with
|
|
184
|
+
`recursive`) whose names match `patterns`, by default all JSON, NDJSON and XML files, also gzip compressed. Hidden
|
|
185
|
+
files are skipped. A missing directory is an error.
|
|
186
|
+
- **Formats:** `.json` (a resource), `.ndjson` (a resource per line), `.xml` (a resource in FHIR XML, needs the extra
|
|
187
|
+
`xml`), each optionally `.gz`. Bundles in files give the resources of their entries (`unpack_bundles`).
|
|
188
|
+
- **Results:** every resource of the files, or only those of `resource_types`; the others are then only used when
|
|
189
|
+
they are referenced. Each file is one page: with `bundling.bundle.size: 0`, one Bundle per file.
|
|
190
|
+
- **References** are resolved from the files, by type and id. A resource that occurs twice is used once, with a
|
|
191
|
+
warning. References written as `Type?<parameters>` can't be resolved from files (see
|
|
192
|
+
[Important behaviour](#important-behaviour)).
|
|
193
|
+
- **Invalid files** stop the run (`on_error: raise`), or are skipped with a warning (`on_error: skip`).
|
|
194
|
+
- **`-q`** can't be used with files.
|
|
195
|
+
|
|
196
|
+
### Exit codes and logging
|
|
197
|
+
|
|
198
|
+
- **0** means success.
|
|
199
|
+
- **2** means an invalid command line or configuration, e.g. no search for a server source, or `-q` with a files
|
|
200
|
+
source. The configuration error is logged with all problems found.
|
|
201
|
+
- **1** means an error while running, e.g. an unreachable server, an invalid file, a failing rule, a missing
|
|
202
|
+
reference, or a resource without the identifier its request needs (see [Important behaviour](#important-behaviour)).
|
|
203
|
+
The error is printed with a traceback.
|
|
204
|
+
|
|
205
|
+
Logging goes to standard error, with timestamps, at level `INFO`. Each processed resource, fetched reference and
|
|
206
|
+
written Bundle is logged. At the end, the number of written and rejected Bundles is logged.
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
## Configuration
|
|
210
|
+
|
|
211
|
+
The configuration is a YAML file. **[config-example.yaml](config-example.yaml) documents every setting** with its
|
|
212
|
+
default. Copy it to `config.yaml` and adapt it. When fhirsync starts, it validates the whole file and reports all
|
|
213
|
+
problems at once:
|
|
214
|
+
|
|
215
|
+
```
|
|
216
|
+
Invalid configuration in config.yaml:
|
|
217
|
+
source.auth: auth_url, realm, client_id also need client_secret
|
|
218
|
+
bundling.bundle.sise: unknown configuration variable, did you mean 'size'?
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
- **Errors** stop fhirsync before anything is fetched or sent: unknown variables (with a suggestion for typos),
|
|
222
|
+
incomplete settings (half an authentication, a target mode without its URL or directory), invalid values, and
|
|
223
|
+
invalid rules.
|
|
224
|
+
- **Warnings** are logged for settings that have no effect, e.g. `target.outdir: ignored, not used in mode server`.
|
|
225
|
+
fhirsync continues.
|
|
226
|
+
|
|
227
|
+
### Secrets and environment variables
|
|
228
|
+
|
|
229
|
+
Any text value can use `${NAME}` or `${NAME:-default}`:
|
|
230
|
+
|
|
231
|
+
```yaml
|
|
232
|
+
source:
|
|
233
|
+
auth:
|
|
234
|
+
auth_url: https://authserver/auth
|
|
235
|
+
realm: REALM
|
|
236
|
+
client_id: fhirsync
|
|
237
|
+
client_secret: ${GICS_CLIENT_SECRET}
|
|
238
|
+
target:
|
|
239
|
+
mode: server
|
|
240
|
+
base_url: ${FHIR_TARGET_URL:-https://fhirserver/fhir}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
- **An unset variable without a default** is an error that names the variable and the setting.
|
|
244
|
+
- **Substitution happens after the YAML is read**, so values may contain any characters.
|
|
245
|
+
- **Inside inline mappings** (`{ … }`), quote values that contain `${…}`.
|
|
246
|
+
|
|
247
|
+
### Settings reference
|
|
248
|
+
|
|
249
|
+
Paths are written as `section.key`. "common" in a per-type section applies to all resource types, and a type's own
|
|
250
|
+
setting overrides it (see below).
|
|
251
|
+
|
|
252
|
+
**`common`**: defaults for the TLS connections to source and target
|
|
253
|
+
|
|
254
|
+
| Setting | Default | Meaning |
|
|
255
|
+
|---|---|---|
|
|
256
|
+
| `common.ca_file` | none (system CAs) | CA certificate file (PEM). If set, only this file is trusted. |
|
|
257
|
+
| `common.rfc5280` | `true` | strict RFC 5280 certificate checks; `false` for non-conforming internal CAs |
|
|
258
|
+
|
|
259
|
+
**`source`**: where the resources come from (required)
|
|
260
|
+
|
|
261
|
+
| Setting | Default | Meaning |
|
|
262
|
+
|---|---|---|
|
|
263
|
+
| `source.type` | `server` | `server`: a FHIR server; `files`: FHIR files. The settings of the other type are errors. |
|
|
264
|
+
|
|
265
|
+
With `type: server`:
|
|
266
|
+
|
|
267
|
+
| Setting | Default | Meaning |
|
|
268
|
+
|---|---|---|
|
|
269
|
+
| `source.base_url` | required | base URL of the FHIR endpoint |
|
|
270
|
+
| `source.auth.username`, `.password` | none | basic authentication; both or neither |
|
|
271
|
+
| `source.auth.auth_url`, `.realm`, `.client_id`, `.client_secret` | none | OAuth 2.0 via Keycloak; all four or none. With username/password: password flow, otherwise client credentials flow |
|
|
272
|
+
| `source.ca_file`, `source.rfc5280` | `common.*` | TLS settings for this server |
|
|
273
|
+
| `source.next_param` | none | query parameter for offset paging, for servers without `next` links (gICS), e.g. `_offset` |
|
|
274
|
+
| `source.request_params.<common\|Type>.<param>` | none | defaults for every search of the type (`searches`, `-q`, applications, `Type?…` references); a search's own parameters take precedence, except in `Type?…` references; a list value repeats the parameter |
|
|
275
|
+
| `source.searches` | none | the searches of a run, each `<Type>` or `<Type>?<parameters>`; `-q` replaces them. Unknown resource types are errors. |
|
|
276
|
+
|
|
277
|
+
With `type: files`:
|
|
278
|
+
|
|
279
|
+
| Setting | Default | Meaning |
|
|
280
|
+
|---|---|---|
|
|
281
|
+
| `source.directory` | required | the directory with the FHIR files |
|
|
282
|
+
| `source.patterns` | `*.json`, `*.ndjson`, `*.xml`, `*.json.gz`, `*.ndjson.gz`, `*.xml.gz` | file name patterns (`*`, `?`, `[seq]`, case-sensitive) of the files to read |
|
|
283
|
+
| `source.recursive` | `false` | also read the files of all subdirectories |
|
|
284
|
+
| `source.include_hidden` | `false` | also read hidden files (`.name`) and hidden subdirectories |
|
|
285
|
+
| `source.follow_symlinks` | `false` | also read subdirectories that are symbolic links |
|
|
286
|
+
| `source.unpack_bundles` | `true` | a Bundle in a file gives the resources of its entries; `false`: the Bundle itself |
|
|
287
|
+
| `source.on_error` | `raise` | an invalid file, line or resource: `raise` stops the run, `skip` logs a warning and continues |
|
|
288
|
+
| `source.resource_types` | all | the resource types that are results; the others are only used as reference targets. Unknown resource types are errors. |
|
|
289
|
+
|
|
290
|
+
**`target`**: where the Bundles go (required)
|
|
291
|
+
|
|
292
|
+
| Setting | Default | Meaning |
|
|
293
|
+
|---|---|---|
|
|
294
|
+
| `target.mode` | required | `server` or `file` |
|
|
295
|
+
| `target.base_url` | required with `server` | base URL of the target FHIR endpoint |
|
|
296
|
+
| `target.outdir` | required with `file` | directory for the Bundle files `<identifier>.json` |
|
|
297
|
+
| `target.auth.*` | none | as `source.auth`, only with `server` |
|
|
298
|
+
| `target.ca_file`, `target.rfc5280` | `common.*` | TLS settings, only with `server` |
|
|
299
|
+
|
|
300
|
+
**`bundling`**: Bundles and requests
|
|
301
|
+
|
|
302
|
+
| Setting | Default | Meaning |
|
|
303
|
+
|---|---|---|
|
|
304
|
+
| `bundling.bundle.type` | `transaction` | `transaction`, `batch` or `collection` |
|
|
305
|
+
| `bundling.bundle.size` | `1` | results per Bundle, plus their referenced resources; `0` for one Bundle per search page or file |
|
|
306
|
+
| `bundling.bundle.identifier_system` | `http://www.uniklinikum-jena.de/fhir/sid/consentBundle` | system of `Bundle.identifier`; its value is a random UUID |
|
|
307
|
+
| `bundling.resources.<common\|Type>.request.method` | `put` | `put` (create or update) or `post` (create) |
|
|
308
|
+
| `bundling.resources.<common\|Type>.request.url` | `identifier` | for `put`: `identifier` (`PUT Type?identifier=…`, needs an identifier) or `id` (`PUT Type/<source id>`) |
|
|
309
|
+
| `bundling.resources.<common\|Type>.request.condition` | none | for `post`: `ifNoneExist`, create only if no resource with the identifier that counts exists |
|
|
310
|
+
| `bundling.resources.<common\|Type>.reference_mode` | `identifier` | `identifier`: rewrite references from this type to resources in the Bundle to `Type?identifier=…`; `as_is`: leave them |
|
|
311
|
+
| `bundling.resources.<common\|Type>.ignore_references_to` | `[]` | resource types whose references from this type are not followed (and not rewritten) |
|
|
312
|
+
| `bundling.resources.<common\|Type>.on_missing_reference` | `error` | a reference from this type the source doesn't have: `error` stops the run, `warning` logs it and leaves the reference unchanged |
|
|
313
|
+
| `bundling.resources.<common\|Type>.identifier_system` | none | the system of the identifier that counts for this type's requests, `ifNoneExist` conditions and references to it; none: the first identifier |
|
|
314
|
+
| `bundling.resources.<common\|Type>.on_missing_identifier_system` | `error` | a resource without an identifier of `identifier_system`: `error` stops the run, `first` logs a warning and uses its first identifier. Only with `identifier_system`. |
|
|
315
|
+
|
|
316
|
+
**`processing`**: filtering and changing resources, see [docs/rules.md](docs/rules.md)
|
|
317
|
+
|
|
318
|
+
| Setting | Default | Meaning |
|
|
319
|
+
|---|---|---|
|
|
320
|
+
| `processing.identifier_systems.<Type>` | none | identifier system per resource type, for `identifierSystem('<Type>')` in rules |
|
|
321
|
+
| `processing.rules.<Type\|"*">.<filter\|transform\|finalize>` | none | the rules |
|
|
322
|
+
|
|
323
|
+
### `common` and per-type settings
|
|
324
|
+
|
|
325
|
+
In `source.request_params` and `bundling.resources`, the key `common` applies to all resource types, and a resource
|
|
326
|
+
type's own key overrides it:
|
|
327
|
+
|
|
328
|
+
- **Single values** (`method`, `url`, `condition`, `reference_mode`, `on_missing_reference`, `identifier_system`,
|
|
329
|
+
`on_missing_identifier_system`) are overridden one by one. With
|
|
330
|
+
`common.request: {method: put, url: id}` and `Patient.request: {url: identifier}`, Patient gets `put` with
|
|
331
|
+
`identifier`.
|
|
332
|
+
- **Lists** (`ignore_references_to`) are replaced as a whole. `Encounter: {ignore_references_to: []}` follows all
|
|
333
|
+
references, even if `common` ignores some.
|
|
334
|
+
- **Search parameters** are merged parameter by parameter. With `common: {_count: '10', _lastUpdated: ge2025}` and
|
|
335
|
+
`Consent: {_lastUpdated: ge2026}`, Consent searches use `_count=10&_lastUpdated=ge2026`.
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
## Processing rules
|
|
339
|
+
|
|
340
|
+
Rules filter and change the resources. They are written per resource type and phase, and use
|
|
341
|
+
[FHIRPath](https://hl7.org/fhirpath/) to read values:
|
|
342
|
+
|
|
343
|
+
```yaml
|
|
344
|
+
processing:
|
|
345
|
+
identifier_systems:
|
|
346
|
+
Patient: http://hospital.example/sid/patient
|
|
347
|
+
rules:
|
|
348
|
+
Consent:
|
|
349
|
+
filter:
|
|
350
|
+
- skip: {when: "status != 'active'"}
|
|
351
|
+
- extract: {name: pat_id, from: patient.display, pattern: '^\s*\S+\s+(\d{10})\s*$'}
|
|
352
|
+
transform:
|
|
353
|
+
- set: {path: patient.reference, value: "Patient?identifier={identifierSystem('Patient')}|{%pat_id}"}
|
|
354
|
+
- delete: patient.display
|
|
355
|
+
"*":
|
|
356
|
+
finalize:
|
|
357
|
+
- delete: [id, meta.lastUpdated]
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
**[docs/rules.md](docs/rules.md)** describes the phases, every operation, the expression language and its pitfalls,
|
|
361
|
+
with a cookbook of tested real world examples. Examples include skipping resources, stable identifiers from natural
|
|
362
|
+
keys, pseudonymisation, tagging, and custom Python functions.
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
## Using fhirsync as a library
|
|
366
|
+
|
|
367
|
+
Applications add `fhir-sync` as a dependency and import the package `fhirsync`:
|
|
368
|
+
|
|
369
|
+
```python
|
|
370
|
+
from fhirsync import ConfigError, ResourceBundler, load_config
|
|
371
|
+
|
|
372
|
+
config = load_config("config.yaml") # raises ConfigError with all problems
|
|
373
|
+
bundler = ResourceBundler(config) # one bundler can run many searches
|
|
374
|
+
result = bundler.fetch_resources("Consent", params={
|
|
375
|
+
"patient.identifier": "https://ths-greifswald.de/fhir/gics/identifiers/Pseudonym|1000000001",
|
|
376
|
+
})
|
|
377
|
+
print(result.bundles, result.entries, result.rejected)
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
| API | Meaning |
|
|
381
|
+
|---|---|
|
|
382
|
+
| `load_config(path)` | loads and validates a fhirsync configuration file, as the command does; raises `ConfigError` |
|
|
383
|
+
| `ResourceBundler(config)` | the bundler for a configuration; for a files source, all files are read here |
|
|
384
|
+
| `ResourceBundler.run(searches=None)` | runs like the command: the searches of a server source (`searches` instead of `source.searches`, like `-q`), or the files of a files source; returns the added-up `SyncResult` |
|
|
385
|
+
| `ResourceBundler.fetch_resources(search, params=None)` | runs one search of a server source and returns a `SyncResult` |
|
|
386
|
+
| `SyncResult` | `bundles` and `entries` written to files or accepted by the target, `rejected`: Bundles the target answered with 404 |
|
|
387
|
+
| `load_yaml(path, model)`, `ConfigModel` | load an application's own YAML configuration like fhirsync's (see below) |
|
|
388
|
+
| `Config`, `ConfigError`, `RuleError`, `MissingReferenceError` | the configuration model and the errors |
|
|
389
|
+
|
|
390
|
+
- **Search parameters** in `params` are not URL encoded; a list value repeats the parameter. They take precedence
|
|
391
|
+
over the parameters in `search`, which take precedence over `source.request_params`. As for `searches`: pass what
|
|
392
|
+
is specific to this search, and leave what applies to every search of the type to `source.request_params`.
|
|
393
|
+
- **Errors** are raised, as with the command: `RuleError` for a failing rule, `MissingReferenceError` for a missing
|
|
394
|
+
reference with `on_missing_reference: error`, `ValueError` for a resource without the identifier its request needs
|
|
395
|
+
and for searches with a files source, `requests` exceptions for connection and HTTP errors, and dic-helper-toolkit's
|
|
396
|
+
`FHIRFileError` for an invalid file with `on_error: raise`. A 404 from the target is only counted in `rejected`.
|
|
397
|
+
- **Logging** uses the loggers `fhirsync.*` and is not configured by fhirsync; that is up to the application.
|
|
398
|
+
- **Own configuration files:** a class derived from `ConfigModel` and loaded with `load_yaml(path, Model)` gets the
|
|
399
|
+
same behaviour as fhirsync's file: `${…}` environment variables, all errors at once with suggestions for typos, and
|
|
400
|
+
warnings for the settings its `ignored_settings()` reports.
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
## Important behaviour
|
|
404
|
+
|
|
405
|
+
> **Without rules, no resource is changed.** Resources are still fetched, bundled and sent, but exactly as the source
|
|
406
|
+
> delivered them, including the source server's `id` and `meta.lastUpdated`. Many servers reject conditional updates
|
|
407
|
+
> whose body contains a different `id`. Almost every configuration therefore needs at least:
|
|
408
|
+
>
|
|
409
|
+
> ```yaml
|
|
410
|
+
> processing:
|
|
411
|
+
> rules:
|
|
412
|
+
> "*":
|
|
413
|
+
> finalize:
|
|
414
|
+
> - delete: [id, meta.lastUpdated]
|
|
415
|
+
> ```
|
|
416
|
+
|
|
417
|
+
> **A resource without an identifier stops the whole run** when its request is identifier based
|
|
418
|
+
> (`request.url: identifier`, the default, or `condition: ifNoneExist`). The error is
|
|
419
|
+
> `Resource <Type>/<id> has no identifier.`. This is intended: no resource is written to the target without a stable
|
|
420
|
+
> identity. Resources whose source has no identifier need a `set_identifier` rule; gICS's `consent-status` Consents
|
|
421
|
+
> are an example. Otherwise use `request.url: id` for that type.
|
|
422
|
+
|
|
423
|
+
> **The first identifier counts,** unless `identifier_system` is configured for the resource type. Request URLs,
|
|
424
|
+
> `ifNoneExist` conditions and rewritten references all use a resource's first identifier, or the one with the
|
|
425
|
+
> configured system. A resource without an identifier of that system stops the run, unless
|
|
426
|
+
> `on_missing_identifier_system: first` is set. This also applies to resource types with a single identifier, such as
|
|
427
|
+
> `QuestionnaireResponse`.
|
|
428
|
+
|
|
429
|
+
> **A failing rule stops the run**, e.g. a template expression without a result. Invalid rules are already reported
|
|
430
|
+
> at startup.
|
|
431
|
+
|
|
432
|
+
> **A missing reference stops the run** by default (`on_missing_reference: error`): a referenced `Type/id` the
|
|
433
|
+
> source server answers with 404, or that the files don't contain, and a `Type?<parameters>` reference whose search
|
|
434
|
+
> finds nothing (with files: every such reference). The error names the reference, the resource and the setting.
|
|
435
|
+
> With `on_missing_reference: warning` for the referencing type, the reference is logged and left unchanged.
|
|
436
|
+
|
|
437
|
+
- **Ignored references are not rewritten.** A reference to a type in `ignore_references_to` keeps pointing to the
|
|
438
|
+
source server's id. Set it with a rule if the target needs it, as the gICS rules do for `Consent.patient`.
|
|
439
|
+
- **Skipping leaves references unchanged.** If a `filter` rule skips a referenced resource, the reference to it is
|
|
440
|
+
not rewritten.
|
|
441
|
+
- **Duplicates:** a resource that several results reference is only added once per Bundle.
|
|
442
|
+
- **Empty Bundles,** e.g. when all resources were skipped, are not written.
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
## Syncing gICS consents
|
|
446
|
+
|
|
447
|
+
[config-example.yaml](config-example.yaml) is a complete setup for syncing the `consent-status` Consents of a gICS
|
|
448
|
+
domain `MII`. Its rules reproduce the processing that was built into fhirsync 0.7 and older (see
|
|
449
|
+
[docs/rules.md, section 7.2](docs/rules.md#72-a-complete-rule-set-gics-consents)).
|
|
450
|
+
|
|
451
|
+
The separate project **gics-consent-sync** syncs consents event-driven, from gICS's MQTT notifications. Its README also
|
|
452
|
+
describes what this setup expects from gICS, the tested gICS versions, and how to convert configurations of fhirsync
|
|
453
|
+
0.7 and older.
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
## Upgrading from fhirsync 0.8 and 0.9
|
|
457
|
+
|
|
458
|
+
fhirsync 0.10 adds FHIR files as a source (`source.type: files`) and searches in the configuration
|
|
459
|
+
(`source.searches`). Existing configurations stay valid: `source.type` defaults to `server`.
|
|
460
|
+
|
|
461
|
+
- **`-q` is optional:** without it, the searches of `source.searches` are run. A server source without both is an
|
|
462
|
+
invalid command line (exit code 2) instead of the former "required argument" error.
|
|
463
|
+
- **Missing references stop the run by default** (`on_missing_reference: error`). A `Type/id` reference the server
|
|
464
|
+
answers with 404 already stopped the run, now with a `MissingReferenceError` instead of an `HTTPError`. **New:** a
|
|
465
|
+
`Type?<parameters>` reference whose search finds nothing stops the run too; before, it was silently left unchanged.
|
|
466
|
+
Set `bundling.resources.<common|Type>.on_missing_reference: warning` for the previous behaviour.
|
|
467
|
+
- **References in lists are followed:** References held directly in a list, e.g. `Observation.performer` or
|
|
468
|
+
`Consent.organization`, were not found before, so they were neither followed nor rewritten. Now they are, like
|
|
469
|
+
every other reference; a missing one stops the run by default (see above). The gICS rules are not affected: gICS
|
|
470
|
+
Consents only carry the organization's name.
|
|
471
|
+
- **Choosing the identifier:** `bundling.resources.<common|Type>.identifier_system` picks the identifier with that
|
|
472
|
+
system for requests and references, instead of the first one. Without it nothing changes.
|
|
473
|
+
- **Library:** `ResourceBundler.run()` runs like the command; `fetch_resources()` is still there for single searches
|
|
474
|
+
of a server source. `ResourceBundler.source_client` is `None` for a files source.
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
## Upgrading from fhirsync 0.7 and older
|
|
478
|
+
|
|
479
|
+
fhirsync 0.8 replaces the INI configuration with YAML, and the processing that was built in is now written as rules.
|
|
480
|
+
Sync mode (`fhirsync -s`) moved to the separate project gics-consent-sync, which uses its own configuration file for
|
|
481
|
+
the MQTT settings. Its converter turns an INI configuration into both files:
|
|
482
|
+
|
|
483
|
+
```sh
|
|
484
|
+
# in gics-consent-sync
|
|
485
|
+
uv run python tools/convert_ini_config.py config.ini config.yaml gics-consent-sync.yaml
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
Without sync mode, only `config.yaml` is needed. Other changes for query mode:
|
|
489
|
+
- Strict RFC 5280 certificate checks are on by default; set `rfc5280: false` for non-conforming internal CAs.
|
|
490
|
+
- Offset paging is only used when `next_param` is set.
|
|
491
|
+
- `ifNoneExist` conditions use the FHIR standard form `identifier=system|value`.
|
|
492
|
+
- Common and type-specific search parameters are merged parameter by parameter. Before, the type-specific ones
|
|
493
|
+
replaced the common ones completely.
|
|
494
|
+
- Code that imported the package `sync` imports `fhirsync` now.
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
## Testing
|
|
498
|
+
|
|
499
|
+
```sh
|
|
500
|
+
uv run pytest tests
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
Each test checks one documented behaviour. A comment above it names the section of this README,
|
|
504
|
+
[docs/rules.md](docs/rules.md) or [config-example.yaml](config-example.yaml) it verifies.
|
|
505
|
+
|
|
506
|
+
- **`tests/config/`:** loading and validating configurations, defaults, errors, warnings, environment variables
|
|
507
|
+
- **`tests/rules/`:** the rule engine: operations, templates, FHIRPath, variables, startup checks
|
|
508
|
+
- **`tests/cookbook/`:** the cookbook examples of [docs/rules.md](docs/rules.md)
|
|
509
|
+
- **`tests/bundling/`:** the bundler with rules, references, identifiers and requests, on recorded gICS responses
|
|
510
|
+
- **`tests/sources/`:** the source types: files, the searches of a server source, missing references
|
|
511
|
+
- **`tests/library/`:** the library API
|
|
512
|
+
- **`tests/cli/`:** the `fhirsync` command: options, exit codes, logging
|
|
513
|
+
- **`tests/connections/`:** authentication and TLS of the source and target connections
|
|
514
|
+
- **`tests/project/`:** project structure, Python version and dependencies
|
|
515
|
+
|
|
516
|
+
The recorded gICS responses in `tests/data/` come from gics-consent-sync's real world data tests.
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
## Project structure
|
|
520
|
+
|
|
521
|
+
```
|
|
522
|
+
fhirsync/
|
|
523
|
+
__init__.py the library API
|
|
524
|
+
cli.py the fhirsync command
|
|
525
|
+
config.py configuration: YAML loading, validation, environment variables
|
|
526
|
+
bundler.py results, references, Bundles, requests, output
|
|
527
|
+
sources.py the source types: FHIR server and FHIR files
|
|
528
|
+
rules.py the processing rule engine
|
|
529
|
+
docs/
|
|
530
|
+
rules.md processing rules and expression language
|
|
531
|
+
tests/
|
|
532
|
+
config/ rules/ cookbook/ bundling/ tests, see Testing
|
|
533
|
+
sources/ library/ cli/ connections/ project/
|
|
534
|
+
support.py, conftest.py shared test functions and fixtures
|
|
535
|
+
data/ resources, rules and recorded responses used by the tests
|
|
536
|
+
ci/
|
|
537
|
+
verify_published_package.py CI check of the package published on PyPI
|
|
538
|
+
config-example.yaml documented example configuration
|
|
539
|
+
ARCHITECTURE.md how fhirsync is built, for developers
|
|
540
|
+
.github/workflows/ci.yml CI (GitHub Actions): build, test, publish to PyPI, tag the release
|
|
541
|
+
```
|