Skip to content

Commit

Permalink
feat: add watch command
Browse files Browse the repository at this point in the history
  • Loading branch information
ruscoder committed Nov 22, 2024
1 parent bfd9ff5 commit e2ce7a8
Show file tree
Hide file tree
Showing 8 changed files with 348 additions and 19 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# IDE
.DS_Store
.idea
.history

# Build
dist

# Versioning
node_modules

24 changes: 18 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

## How it works?
The server reads all `yaml` and `json` files from `resources` directory.
Rsources directory should have subdirectories with names equal resource types:
Resources directory should have subdirectories with names equal resource types:
```markdown
resources/
├── Patient/
Expand All @@ -31,22 +31,34 @@ resources/
- `GET /$index` operation returns a map of all resources in format `<resource_type>:<id>`


## How to use?
## Usage

1. Organize resources in a directory
2. Adjust source destination in `Dockerfile.resources` if required
3. Option A: Run a container

### Server
1. Option A: Run a container
```bash
docker run -p 8002:8000 -v ./resources:/app/resources bedasoftware/fhirsnake
```
3. Option B: Build an image using the base image
2. Option B: Adjust source destination in `Dockerfile.resources` if required
2.1. Build an image using the base image
```bash
docker build -t fhirsnake-resources:latest -f Dockerfile.resources .
docker run -p 8000:8000 fhirsnake-resources
```
4. Option C: Export resources as .ndjson or ndjson.gz

### Export
1. Export resources as .ndjson or ndjson.gz
```bash
docker run -v ./resources:/app/resources -v ./output:/output bedasoftware/fhirsnake export --output /output/seeds.ndjson.gz
```

### Watch
1. Watch resources for changes and send as PUT requests to external fhir server
```bash
docker run -v ./resources:/app/resources -v ./output:/output bedasoftware/fhirsnake watch --external-fhir-server-url http://localhost:8080 --external-fhir-server-header "Authorization: Token token"
```


## Contribution and feedback
Please, use [Issues](https://github.com/beda-software/fhirsnake/issues)
Expand Down
2 changes: 1 addition & 1 deletion entrypoint.sh
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
#!/bin/sh
poetry run python3 cli.py $@
poetry run python3 cli.py "$@"
26 changes: 26 additions & 0 deletions fhirsnake/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import ndjson
import uvicorn
from initial_resources import initial_resources
from watch import start_watcher

logging.basicConfig(level=logging.INFO)

Expand All @@ -21,6 +22,7 @@ def main() -> None:
help="Specify the output filename",
)

# TODO: add "serve" as alias
server_parser = subparsers.add_parser("server", help="Run fhirsnake FHIR server")
server_parser.add_argument(
"--host",
Expand All @@ -35,6 +37,21 @@ def main() -> None:
help="Port",
)

watch_parser = subparsers.add_parser("watch", help="Watch resources changes and send them to FHIR server")
watch_parser.add_argument(
"--external-fhir-server-url",
required=True,
type=str,
help="External FHIR Server URL",
)

watch_parser.add_argument(
"--external-fhir-server-header",
required=False,
type=str,
action="append",
help="External FHIR Server header",
)
args = parser.parse_args()

if args.command == "export":
Expand All @@ -43,13 +60,22 @@ def main() -> None:
if args.command == "server":
server(args.host, args.port)

if args.command == "watch":
watch(args.external_fhir_server_url, args.external_fhir_server_header)


def server(host: str, port: int) -> None:
config = uvicorn.Config("server:app", host=host, port=port)
server = uvicorn.Server(config)
server.run()


def watch(url: str, headers: list[str] | None):
headers = headers or []
headers = {v.split(":", 1)[0]: v.split(":", 1)[1] for v in headers}
start_watcher(url, headers)


def export_resources(output: str) -> None:
gzipped = output.endswith(".gz")
resources_list = flatten_resources(initial_resources)
Expand Down
34 changes: 23 additions & 11 deletions fhirsnake/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,27 @@ def load_resources_by_ids(abs_path, resource_type):
path = os.path.join(abs_path, resource_type)

for filename in os.listdir(path):
file_ext = filename[-4:]
if filename.endswith(".yaml") or filename.endswith(".json"):
resource_id = filename[:-5]
with open(os.path.join(path, filename)) as f:
if file_ext == "yaml":
resource = yaml.safe_load(f)
elif file_ext == "json":
resource = json.load(f)
resource["resourceType"] = resource_type
resource["id"] = resource_id
resources[resource_id] = resource
resource = load_resource(os.path.join(path, filename))
if not resource:
continue
resources[resource["id"]] = resource
return resources


def load_resource(path: str):
# path: /path/to/resources/Patient/id.json
resource_type = path.split("/")[-2]
file_name = path.split("/")[-1]

resource_id, file_ext = file_name.rsplit(".", 1)
if file_ext not in ("yaml", "yml", "json"):
return None

with open(path) as f:
if file_ext in ("yaml", "yml"):
resource = yaml.safe_load(f)
elif file_ext == "json":
resource = json.load(f)
resource["resourceType"] = resource_type
resource["id"] = resource_id
return resource
70 changes: 70 additions & 0 deletions fhirsnake/watch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import logging
import time

import requests
from files import load_resource
from initial_resources import RESOURCE_DIR
from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer

logging.basicConfig(level=logging.INFO)


class FileChangeHandler(FileSystemEventHandler):
def __init__(
self, external_fhir_server_url: str, external_fhir_server_headers: dict[str, str], *args, **kwargs
) -> None:
self.external_fhir_server_url = external_fhir_server_url
self.external_fhir_server_headers = external_fhir_server_headers
super().__init__(*args, **kwargs)

def on_modified(self, event):
if event.is_directory:
return

file_path = event.src_path
logging.info("Detected change in %s", file_path)
self.process_file(file_path)

def process_file(self, file_path):
try:
resource = load_resource(file_path)
except Exception:
logging.exception("Unable to load resource %s", file_path)
return

if resource is None:
logging.error("Unable to load resource %s", file_path)
return

resource_type = resource["resourceType"]
resource_id = resource["id"]
url = f"{self.external_fhir_server_url}/{resource_type}/{resource_id}"

try:
response = requests.put(
url, json=resource, headers={"Content-Type": "application/json", **self.external_fhir_server_headers}
)
if response.status_code >= 400:
logging.error(
"Unable to update %s via %s (%s): %s", file_path, url, response.status_code, response.text()
)
else:
logging.info("Updated %s via %s (%s)", file_path, url, response.status_code)
except requests.RequestException:
logging.exception("Failed to PUT %s via %s", file_path, url)


def start_watcher(external_fhir_server_url: str, external_fhir_server_headers: dict[str, str]):
event_handler = FileChangeHandler(external_fhir_server_url, external_fhir_server_headers)
observer = Observer()
observer.schedule(event_handler, RESOURCE_DIR, recursive=True)
observer.start()
logging.info("Watching directory %s", RESOURCE_DIR)

try:
while True:
time.sleep(5)
except KeyboardInterrupt:
observer.stop()
observer.join()
Loading

0 comments on commit e2ce7a8

Please sign in to comment.