Skip to content

Commit 885bdaf

Browse files
authored
feat: add support for input dirs (multiple for watch and export)
1 parent c4c6775 commit 885bdaf

8 files changed

Lines changed: 253 additions & 179 deletions

File tree

README.md

Lines changed: 85 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
**fhirsnake** is a minimalistic FHIR server that serve yaml and json files as FHIR resources
99

1010
## How it works?
11-
The server reads all `yaml` and `json` files from `resources` directory.
12-
Resources directory should have subdirectories with names equal resource types:
11+
The server reads all `yaml` and `json` files from an input directory (`resources` by default).
12+
The input directory should have subdirectories with names equal to resource types:
1313
```markdown
1414
resources/
1515
├── Patient/
@@ -19,10 +19,12 @@ resources/
1919
├── Questionnaire/
2020
│ ├── questionnaire1.yaml
2121
│ ├── questionnaire2.yaml
22-
│ └── sudbir/
22+
│ └── subdir/
2323
│ └── questionnaire3.yaml
2424
```
2525

26+
Use the `--input` flag to specify a custom input directory. For `export` and `watch` commands, `--input` can be passed multiple times to load resources from several directories.
27+
2628
## Environment variable substitution
2729

2830
To use environment variables in resources, you can use the syntax `${VAR_NAME}`.
@@ -44,32 +46,92 @@ NOTE: The syntax `$VAR` without braces is not supported because it might be used
4446
1. Organize resources in a directory
4547

4648
### Server
47-
1. Option A: Run a container
48-
```bash
49-
docker run -p 8002:8000 -v ./resources:/app/resources bedasoftware/fhirsnake
50-
```
51-
2. Option B: Adjust source destination in `Dockerfile.resources` if required
52-
2.1. Build an image using the base image
53-
```bash
54-
docker build -t fhirsnake-resources:latest -f Dockerfile.resources .
55-
docker run -p 8000:8000 fhirsnake-resources
56-
```
49+
50+
```bash
51+
docker run \
52+
-p 8002:8000 \
53+
-v ./resources:/resources \
54+
bedasoftware/fhirsnake server --input /resources
55+
```
56+
57+
Or build a custom image with `Dockerfile.resources`:
58+
```bash
59+
docker build -t bedasoftware/fhirsnake -f Dockerfile.resources .
60+
docker run -p 8000:8000 fhirsnake-resources
61+
```
5762

5863
### Export
59-
1. Export resources as .json (Bundle) or .ndjson or ndjson.gz
60-
```bash
61-
docker run -v ./resources:/app/resources -v ./output:/output bedasoftware/fhirsnake export --external-questionnaire-fce-fhir-converter-url=http://host.docker.internal:3000/to-fhir --output /output/seeds.ndjson.gz
62-
```
64+
65+
Export resources as `.json` (Bundle), `.ndjson`, or `.ndjson.gz`:
66+
67+
```bash
68+
docker run \
69+
-v ./resources:/resources \
70+
-v ./output:/output \
71+
bedasoftware/fhirsnake export \
72+
--input /resources \
73+
--output /output/seeds.ndjson.gz
74+
```
75+
76+
Multiple input directories:
77+
```bash
78+
docker run \
79+
-v ./resources1:/resources1 \
80+
-v ./resources2:/resources2 \
81+
-v ./output:/output \
82+
bedasoftware/fhirsnake export \
83+
--input /resources1 \
84+
--input /resources2 \
85+
--output /output/seeds.ndjson.gz
86+
```
87+
88+
With external FCE->FHIR converter:
89+
```bash
90+
docker run \
91+
-v ./resources:/resources \
92+
-v ./output:/output \
93+
bedasoftware/fhirsnake export \
94+
--input /resources \
95+
--output /output/seeds.ndjson.gz \
96+
--external-questionnaire-fce-fhir-converter-url http://host.docker.internal:3000/to-fhir
97+
```
6398

6499
### Watch
65-
1. Watch resources for changes and send as PUT requests to external fhir server
66-
```bash
67-
docker run -v ./resources:/app/resources -v ./output:/output bedasoftware/fhirsnake watch --external-fhir-server-url http://host.docker.internal:8080 --external-fhir-server-header "Authorization: Token token" --external-questionnaire-fce-fhir-converter-url=http://host.docker.internal:3000/to-fhir
68-
```
69100

70-
### Using external questionnaire FCE->FHIR converter
101+
Watch resources for changes and send as PUT requests to an external FHIR server:
102+
103+
```bash
104+
docker run \
105+
-v ./resources:/resources \
106+
bedasoftware/fhirsnake watch \
107+
--input /resources \
108+
--external-fhir-server-url http://host.docker.internal:8080
109+
```
110+
111+
With auth headers:
112+
```bash
113+
docker run \
114+
-v ./resources:/resources \
115+
bedasoftware/fhirsnake watch \
116+
--input /resources \
117+
--external-fhir-server-url http://host.docker.internal:8080 \
118+
--external-fhir-server-header "Authorization: Token token"
119+
```
120+
121+
Multiple input directories:
122+
```bash
123+
docker run \
124+
-v ./resources1:/resources1 \
125+
-v ./resources2:/resources2 \
126+
bedasoftware/fhirsnake watch \
127+
--input /resources1 \
128+
--input /resources2 \
129+
--external-fhir-server-url http://host.docker.internal:8080
130+
```
131+
132+
### External questionnaire FCE->FHIR converter
71133

72-
There's an image `bedasoftware/questionnaire-fce-fhir-converter:latest` available that provides `/to-fhir` endpoint that can be used along with `--external-questionnaire-fce-fhir-converter-url` args for watch and export commands.
134+
The image `bedasoftware/questionnaire-fce-fhir-converter:latest` provides a `/to-fhir` endpoint that can be used with the `--external-questionnaire-fce-fhir-converter-url` flag in `export` and `watch` commands.
73135

74136

75137
## Contribution and feedback

entrypoint.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
#!/bin/sh
2-
poetry run python3 cli.py "$@"
2+
exec poetry run python3 cli.py "$@"

fhirsnake/cli.py

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
import argparse
22
import logging
3+
import os
34

45
import uvicorn
56
from export import export_resources
67
from watch import start_watcher
78

89
logging.basicConfig(level=logging.INFO)
910

11+
DEFAULT_INPUT_DIR = "resources"
12+
13+
root_dir = os.path.dirname(os.path.abspath(__file__))
14+
default_input_dir_abs_path = os.path.join(root_dir, DEFAULT_INPUT_DIR)
15+
1016

1117
def main() -> None:
1218
parser = argparse.ArgumentParser(description="CLI for fhirsnake")
@@ -15,13 +21,15 @@ def main() -> None:
1521

1622
# TODO: add "serve" as alias
1723
server_parser = subparsers.add_parser("server", help="Run fhirsnake FHIR server")
18-
24+
server_parser.add_argument(
25+
"--input",
26+
help=f"Directory of FHIR JSON resources to load (default: {default_input_dir_abs_path})",
27+
)
1928
server_parser.add_argument(
2029
"--host",
2130
default="0.0.0.0",
2231
help="Host",
2332
)
24-
2533
server_parser.add_argument(
2634
"--port",
2735
type=int,
@@ -30,6 +38,11 @@ def main() -> None:
3038
)
3139

3240
export_parser = subparsers.add_parser("export", help="Export resources as .json (Bundle) or .ndjson or .ndjson.gz")
41+
export_parser.add_argument(
42+
"--input",
43+
action="append",
44+
help=f"Input directories to load resources from (default: {default_input_dir_abs_path})",
45+
)
3346
export_parser.add_argument(
3447
"--external-questionnaire-fce-fhir-converter-url",
3548
required=False,
@@ -43,20 +56,23 @@ def main() -> None:
4356
)
4457

4558
watch_parser = subparsers.add_parser("watch", help="Watch resources changes and send them to FHIR server")
59+
watch_parser.add_argument(
60+
"--input",
61+
action="append",
62+
help=f"Input directories to load resources from (default: {default_input_dir_abs_path})",
63+
)
4664
watch_parser.add_argument(
4765
"--external-questionnaire-fce-fhir-converter-url",
4866
required=False,
4967
type=str,
5068
help="External Questionnaire FCE FHIR Converter URL",
5169
)
52-
5370
watch_parser.add_argument(
5471
"--external-fhir-server-url",
5572
required=True,
5673
type=str,
5774
help="External FHIR Server URL",
5875
)
59-
6076
watch_parser.add_argument(
6177
"--external-fhir-server-header",
6278
required=False,
@@ -67,32 +83,55 @@ def main() -> None:
6783
args = parser.parse_args()
6884

6985
if args.command == "server":
70-
server(args.host, args.port)
86+
server(args.input or default_input_dir_abs_path, args.host, args.port)
7187

7288
if args.command == "export":
73-
export(args.output, args.external_questionnaire_fce_fhir_converter_url)
89+
export(
90+
args.input or [default_input_dir_abs_path], args.output, args.external_questionnaire_fce_fhir_converter_url
91+
)
7492

7593
if args.command == "watch":
7694
watch(
95+
args.input or [default_input_dir_abs_path],
7796
args.external_fhir_server_url,
7897
args.external_fhir_server_header,
7998
args.external_questionnaire_fce_fhir_converter_url,
8099
)
81100

82101

83-
def server(host: str, port: int) -> None:
84-
config = uvicorn.Config("server:app", host=host, port=port)
85-
server = uvicorn.Server(config)
86-
server.run()
102+
def validate_input_dirs(input_dirs: list[str]) -> None:
103+
for input_dir in input_dirs:
104+
if not os.path.isdir(input_dir):
105+
raise RuntimeError(f"Required directory '{input_dir}' does not exist. Stopping application.")
106+
87107

108+
def server(input_dir: str, host: str, port: int) -> None:
109+
from server import create_app
88110

89-
def export(output: str, external_questionnaire_fce_fhir_converter_url: str | None):
90-
export_resources(output, external_questionnaire_fce_fhir_converter_url)
111+
validate_input_dirs([input_dir])
112+
config = uvicorn.Config(create_app(input_dir), host=host, port=port)
113+
server = uvicorn.Server(config)
114+
server.run()
91115

92116

93-
def watch(url: str, headers_list: list[str] | None, external_questionnaire_fce_fhir_converter_url: str | None):
94-
headers = {v.split(":", 1)[0]: v.split(":", 1)[1] for v in (headers_list or [])}
95-
start_watcher(url, headers, external_questionnaire_fce_fhir_converter_url)
117+
def export(
118+
input_dirs: list[str],
119+
output: str,
120+
external_questionnaire_fce_fhir_converter_url: str | None,
121+
):
122+
validate_input_dirs(input_dirs)
123+
export_resources(input_dirs, output, external_questionnaire_fce_fhir_converter_url)
124+
125+
126+
def watch(
127+
input_dirs: list[str],
128+
url: str,
129+
headers_list: list[str] | None,
130+
external_questionnaire_fce_fhir_converter_url: str | None,
131+
):
132+
validate_input_dirs(input_dirs)
133+
headers = {v.split(":", 1)[0].strip(): v.split(":", 1)[1].strip() for v in (headers_list or [])}
134+
start_watcher(input_dirs, url, headers, external_questionnaire_fce_fhir_converter_url)
96135

97136

98137
if __name__ == "__main__":

fhirsnake/export.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,20 @@
33

44
import ndjson
55
from converter import convert_resources
6-
from initial_resources import get_initial_resources
6+
from files import load_resources
77
from utils import substitute_env_vars
88

99

10-
def export_resources(output: str, external_questionnaire_fce_fhir_converter_url: str | None) -> None:
10+
def export_resources(
11+
input_dirs: list[str],
12+
output: str,
13+
external_questionnaire_fce_fhir_converter_url: str | None,
14+
) -> None:
1115
is_ndjson = "ndjson" in output
1216
gzipped = output.endswith(".gz")
13-
resources_list = flatten_resources(get_initial_resources())
14-
17+
resources_list = []
18+
for input_dir in input_dirs:
19+
resources_list.extend(flatten_resources(load_resources(input_dir)))
1520
if external_questionnaire_fce_fhir_converter_url:
1621
resources_list = convert_resources(resources_list, external_questionnaire_fce_fhir_converter_url)
1722

@@ -25,7 +30,10 @@ def export_resources(output: str, external_questionnaire_fce_fhir_converter_url:
2530
"entry": [
2631
{
2732
"fullUrl": f"urn:uuid:{resource['resourceType']}:{resource['id']}",
28-
"request": {"method": "PUT", "url": f"/{resource['resourceType']}/{resource['id']}"},
33+
"request": {
34+
"method": "PUT",
35+
"url": f"/{resource['resourceType']}/{resource['id']}",
36+
},
2937
"resource": resource,
3038
}
3139
for resource in resources_list

fhirsnake/initial_resources.py

Lines changed: 0 additions & 21 deletions
This file was deleted.

0 commit comments

Comments
 (0)