Skip to content

Commit

Permalink
Merge pull request #190 from kids-first/custom-search-parameters
Browse files Browse the repository at this point in the history
✨ Add patient us-core-race and us-core-ethnicity search params
  • Loading branch information
znatty22 authored Jul 30, 2024
2 parents febe2c0 + c55b1a9 commit 55e8c58
Show file tree
Hide file tree
Showing 5 changed files with 300 additions and 4 deletions.
60 changes: 60 additions & 0 deletions smilecdr/search_parameters/SearchParameter-us-core-ethnicity.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{
"resourceType": "SearchParameter",
"id": "us-core-ethnicity",
"url": "http://hl7.org/fhir/us/core/SearchParameter/us-core-ethnicity",
"version": "7.0.0",
"name": "USCoreEthnicity",
"status": "active",
"date": "2022-04-14",
"publisher": "HL7 International / Cross-Group Projects",
"contact": [
{
"name": "HL7 International / Cross-Group Projects",
"telecom": [
{
"system": "url",
"value": "http://www.hl7.org/Special/committees/cgp"
},
{
"system": "email",
"value": "[email protected]"
}
]
}
],
"description": "Returns patients with an ethnicity extension matching the specified code.",
"jurisdiction": [
{
"coding": [
{
"system": "urn:iso:std:iso:3166",
"code": "US"
}
]
}
],
"code": "ethnicity",
"base": ["Patient"],
"type": "token",
"expression": "Patient.extension.where(url = 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity').extension.value.code",
"xpathUsage": "normal",
"multipleOr": true,
"_multipleOr": {
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/capabilitystatement-expectation",
"valueCode": "MAY"
}
]
},
"multipleAnd": true,
"_multipleAnd": {
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/capabilitystatement-expectation",
"valueCode": "MAY"
}
]
}
}

60 changes: 60 additions & 0 deletions smilecdr/search_parameters/SearchParameter-us-core-race.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{
"resourceType": "SearchParameter",
"id": "us-core-race",
"url": "http://hl7.org/fhir/us/core/SearchParameter/us-core-race",
"version": "7.0.0",
"name": "USCoreRace",
"status": "active",
"date": "2022-04-14",
"publisher": "HL7 International / Cross-Group Projects",
"contact": [
{
"name": "HL7 International / Cross-Group Projects",
"telecom": [
{
"system": "url",
"value": "http://www.hl7.org/Special/committees/cgp"
},
{
"system": "email",
"value": "[email protected]"
}
]
}
],
"description": "Returns patients with a race extension matching the specified code.",
"jurisdiction": [
{
"coding": [
{
"system": "urn:iso:std:iso:3166",
"code": "US"
}
]
}
],
"code": "race",
"base": ["Patient"],
"type": "token",
"expression": "Patient.extension.where(url = 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-race').extension.value.code",
"xpathUsage": "normal",
"multipleOr": true,
"_multipleOr": {
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/capabilitystatement-expectation",
"valueCode": "MAY"
}
]
},
"multipleAnd": true,
"_multipleAnd": {
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/capabilitystatement-expectation",
"valueCode": "MAY"
}
]
}
}

96 changes: 96 additions & 0 deletions src/bin/load_search_parameters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#!/usr/bin/env python

import os
import argparse
from pprint import pprint

from requests.auth import HTTPBasicAuth

from src.config import (
SEARCH_PARAMETER_DIR,
REINDEX_PAYLOAD,
BASE_URL,
REINDEX_ENDPOINT,
FHIR_APP_ADMIN,
FHIR_APP_ADMIN_PW
)
from src.misc import (
read_json,
send_request
)


def upsert_search_parameters(
client_id, client_secret, search_parameter_dir=SEARCH_PARAMETER_DIR
):
"""
Read search parameters from file, then upsert them in server
Last, reindex the resources so that SearchParameters take effect
"""
print("Loading search parameters")

for fn in os.listdir(search_parameter_dir):
if not fn.endswith(".json"):
continue
filepath = os.path.join(search_parameter_dir, fn)
search_param = read_json(filepath)

# Load search parameter
id_ = search_param["id"]
endpoint = "/".join(
part.strip("/") for part in [BASE_URL, "SearchParameter", id_]
)
resp = send_request(
"put",
endpoint,
json=search_param,
auth=HTTPBasicAuth(client_id, client_secret),
)
print(
f"PUT {endpoint}"
)

# Start reindexing operation
endpoint = f"{BASE_URL}/$reindex"
resp = send_request(
"post",
endpoint,
json=REINDEX_PAYLOAD,
auth=HTTPBasicAuth(client_id, client_secret),
)
pprint(resp.json())


def cli():
"""
CLI for running this script
"""
parser = argparse.ArgumentParser(
description='Load SearchParameters in FHIR server'
)
parser.add_argument(
"--client_id",
default=FHIR_APP_ADMIN,
help="Admin ID to authenticate with FHIR server",
)
parser.add_argument(
"--client_secret",
default=FHIR_APP_ADMIN_PW,
help="Admin secret to authenticate with FHIR server",
)
parser.add_argument(
"--search_parameter_dir",
default=SEARCH_PARAMETER_DIR,
help="Path to dir with SearchParameters",
)
args = parser.parse_args()

upsert_search_parameters(
args.client_id, args.client_secret, args.search_parameter_dir
)

print("✅ Load SearchParameters complete")


if __name__ == "__main__":
cli()
10 changes: 10 additions & 0 deletions src/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.dirname((__file__))))
DATA_DIR = os.path.join(ROOT_DIR, "data", "resources")
SEED_USERS_FILEPATH = os.path.join(ROOT_DIR, "smilecdr/settings", "users.json")
SEARCH_PARAMETER_DIR = os.path.join(ROOT_DIR, "smilecdr/search_parameters")

DOTENV_PATH = find_dotenv()
if DOTENV_PATH:
Expand All @@ -17,6 +18,7 @@
FHIR_APP_ADMIN_PW = os.environ.get("FHIR_APP_ADMIN_PW")
FHIR_TEST_USER_PW = os.environ.get("FHIR_TEST_USER_PW")
USER_MGMNT_ENDPOINT = os.environ.get("USER_MGMNT_ENDPOINT")
REINDEX_ENDPOINT = os.environ.get("REINDEX_ENDPOINT")
KEYCLOAK_PROXY_URL = os.environ.get(
"KEYCLOAK_PROXY_URL", "http://localhost:8081/keycloak-proxy"
)
Expand All @@ -25,3 +27,11 @@
KEYCLOAK_READ_CLIENT_ID = os.environ.get("KEYCLOAK_READ_CLIENT_ID")
KEYCLOAK_READ_CLIENT_SECRET = os.environ.get("KEYCLOAK_READ_CLIENT_SECRET")
KEYCLOAK_ISSUER = os.environ.get("KEYCLOAK_ISSUER")

REINDEX_PAYLOAD= {
"resourceType": "Parameters",
"parameter": [ {
"name": "url",
"valueString": "Patient?"
}]
}
78 changes: 74 additions & 4 deletions src/misc.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
"""
Miscellaneous utility functions
"""
import time
import json
from pprint import pformat

import requests

from src.config import (
BASE_URL,
)


def elapsed_time_hms(start_time):
"""
Expand All @@ -15,3 +16,72 @@ def elapsed_time_hms(start_time):
return time.strftime('%H:%M:%S', time.gmtime(elapsed))


def send_request(method, *args, ignore_status_codes=None, **kwargs):
"""Send http request. Raise exception on status_code >= 300
:param method: name of the requests method to call
:type method: str
:raises: requests.Exception.HTTPError
:returns: requests Response object
:rtype: requests.Response
"""
if isinstance(ignore_status_codes, str):
ignore_status_codes = [ignore_status_codes]

# NOTE: Set timeout so requests don't hang
# See https://requests.readthedocs.io/en/latest/user/advanced/#timeouts
if not kwargs.get("timeout"):
# connect timeout, read timeout
kwargs["timeout"] = (3, 60)
else:
print(
f"⌚️ Applying user timeout: {kwargs['timeout']} (connect, read)"
" seconds to request"
)

requests_op = getattr(requests, method.lower())
status_code = 0
try:
resp = requests_op(*args, **kwargs)
status_code = resp.status_code
resp.raise_for_status()
except requests.exceptions.HTTPError as e:
if ignore_status_codes and (status_code in ignore_status_codes):
pass
else:
body = ""
try:
body = pformat(resp.json())
except:
body = resp.text

msg = (
f"❌ Problem sending {method} request to server\n"
f"{str(e)}\n"
f"args: {args}\n"
f"kwargs: {pformat(kwargs)}\n"
f"{body}\n"
)
print(msg)
raise e

return resp


def read_json(filepath, default=None):
"""
Read JSON file into Python dict. If default is not None and the file
does not exist, then return default.
:param filepath: path to JSON file
:type filepath: str
:param default: default return value if file not found, defaults to None
:type default: any, optional
:returns: your data
:rtype: dict
"""
if (default is not None) and (not os.path.isfile(filepath)):
return default

with open(filepath, "r") as json_file:
return json.load(json_file)

0 comments on commit 55e8c58

Please sign in to comment.